Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ All notable changes to backendkit are documented here. Format:
the app's own identity. It is the cached token of the service-account calls (exchanged again
within 30 s of expiry, one exchange for concurrent callers). Requested by Lakebridge (its consumer
client takes Socrate service tokens without hand-rolling the OAuth exchange).
- `socrate.User.TokenVersion` and `socrate.User.Locked` (`*int`, `*bool`): set by `GetUser` and
`GetUserAsService` from Socrate v1.8.0, nil from lists and older servers. A user token whose
`token_version` claim is lower than `*TokenVersion` was revoked, so a resource server can check
revocation with its cached service token instead of introspecting every user token
(Lakebridge).

### Changed
- A `client_credentials` response without an `access_token` is now an error instead of an empty
Expand Down
2 changes: 1 addition & 1 deletion docs/CLIENT-INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ automatically from `client_id` (cached).
| `DeleteUser(ctx, userID)` | JWT | `error` | removes the user's role in this app. |
| `ResendVerification(ctx, userID)` | JWT | `error` | re-sends the verification email. |
| `ForcePasswordReset(ctx, userID)` | JWT | `error` | triggers a password-reset email. |
| `GetUserAsService(ctx, userID)` | M2M | `*User` | one of the app's members by numeric id (a token's `sub`); **nil,nil** when not a member (Socrate's 404). Needs a Socrate later than v1.5.3; an older one answers with an error, not nil,nil. |
| `GetUserAsService(ctx, userID)` | M2M | `*User` | one of the app's members by numeric id (a token's `sub`); **nil,nil** when not a member (Socrate's 404). Needs a Socrate later than v1.5.3; an older one answers with an error, not nil,nil. From Socrate v1.8.0 `User.TokenVersion` and `User.Locked` are set: a user token whose `token_version` claim is lower than `*TokenVersion` was revoked (sign-out, password change, block), a cheaper check than introspecting every token. |
| `UpdateUserAsService(ctx, userID, UpdateProfileRequest)` | M2M | `*User` | updates profile fields of one of the app's members (name, phone, company, …, `AvatarURL`); never email, password or roles. The account is shared by every app on Socrate, so the change shows everywhere. `ErrUserNotInApp`, `ErrInvalidProfileUpdate` (Socrate's message wrapped). Needs Socrate v1.7.0. |
| `RegisterUser(ctx, CreateUserRequest)` | M2M | `*CreateUserResult` | M2M create+invite, with `Name`; `ErrUserAlreadyExists` on 409. |
| `InviteUserAsService(ctx, ServiceInviteRequest)` | M2M | `*CreateUserResult` | dedicated M2M invite route; no human JWT needed. |
Expand Down
10 changes: 10 additions & 0 deletions socrate/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ type User struct {
LastLogin *time.Time `json:"last_login,omitempty"`
// AvatarURL is the member's picture (Socrate v1.7.0 or later); nil when unset.
AvatarURL *string `json:"avatar_url,omitempty"`
// TokenVersion is the member's current token version (Socrate v1.8.0 or
// later, single-member look-ups GetUser and GetUserAsService only; nil from
// lists and older servers). A user token whose token_version claim is lower
// was revoked: sign-out, password change or reset, block, "revoke all
// tokens", or refresh-token reuse. A single token revoked through
// /oauth/revoke is not reflected; introspection is.
TokenVersion *int `json:"token_version,omitempty"`
// Locked reports whether the account is temporarily locked after failed
// sign-ins (same availability as TokenVersion).
Locked *bool `json:"locked,omitempty"`
}

// UserListResponse is the paginated list returned by ListUsers.
Expand Down
20 changes: 20 additions & 0 deletions socrate/service_user_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,23 @@ func TestGetUserAsService_MissingRouteIsAnError(t *testing.T) {
t.Fatalf("missing route = %+v, %v; want an error naming the Socrate version", u, err)
}
}

// Socrate v1.8.0 adds token_version and locked to the single-member look-up;
// an older server omits them and the fields stay nil.
func TestGetUserAsService_TokenVersionAndLocked(t *testing.T) {
c, _ := serviceRoutesServer(t, func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/7") {
_, _ = w.Write([]byte(`{"id":7,"email":"m@example.test","role":"user","token_version":4,"locked":false}`))
return
}
_, _ = w.Write([]byte(`{"id":9,"email":"old@example.test","role":"user"}`))
})
u, err := c.GetUserAsService(context.Background(), "7")
if err != nil || u == nil || u.TokenVersion == nil || *u.TokenVersion != 4 || u.Locked == nil || *u.Locked {
t.Fatalf("GetUserAsService = %+v, %v; want token_version 4, locked false", u, err)
}
old, err := c.GetUserAsService(context.Background(), "9")
if err != nil || old == nil || old.TokenVersion != nil || old.Locked != nil {
t.Fatalf("older Socrate = %+v, %v; want nil TokenVersion and Locked", old, err)
}
}
Loading