feat(auth) : sync Sudloh profiles and validate sessions

This commit is contained in:
2026-10-06 00:48:29 +07:00 Unverified
parent d6153be50d
commit c856904c12
16 changed files with 520 additions and 32 deletions
+3 -1
View File
@@ -7,4 +7,6 @@ description: Integrate a Sudloh application or API with account.sudloh.com's OID
Read [references/api.md](references/api.md) before implementing a client or protected API. It defines the issuer, endpoints, security checks, credentials, examples, and sign-out behavior. Treat OIDC discovery as the current source of endpoint URLs at runtime.
For Guide and Buzz, request the exact HTTPS callback URL and the assigned `client_id` and `client_secret` from the account service operator. Each app owns its own session. Do not send a browser's account service cookie to an app API or treat an ID token as an API access token.
For any project, determine its exact HTTPS callback URL first. A verified Sudloh Account administrator can sign in at `https://account.sudloh.com/account`, open **OIDC clients**, and create a client with a project name and that callback URL. Copy the new `client_id` and `client_secret` from the result immediately; the secret is shown only at creation. Give them only to that project's server and register a separate client for each project. The command line `client:register` script remains available for Guide and Buzz operators. Each app owns its own session. Do not send a browser's account service cookie to an app API or treat an ID token as an API access token.
For Guide profile syncing and revocation checks, use the account settings URL, UserInfo, and authenticated introspection contract in [references/api.md](references/api.md). The current `openid profile email` grant has no refresh token, so reauthorize when its access token expires.
+16 -9
View File
@@ -7,9 +7,9 @@
- OAuth metadata: `https://account.sudloh.com/api/auth/.well-known/oauth-authorization-server`
- Supported scopes for first-party web clients: `openid profile email`
- Stable user key: the validated ID token's `sub`. Store it as the app's external identity key; do not use email as a primary key.
- Public client registration is disabled. Guide and Buzz are pre-registered, confidential web clients with exact HTTPS redirect URIs, `client_secret_basic`, authorization-code grant, and S256 PKCE. Their consent screen is skipped.
- Public client registration is disabled. A verified Sudloh Account administrator can create a confidential web client for any project at `/admin/clients`, using its exact HTTPS callback URL. Clients use `client_secret_basic`, authorization-code grant, and S256 PKCE. New clients require user consent unless an administrator changes their configuration. Guide and Buzz use separate clients.
The discovery document gives the actual authorization, token, UserInfo, JWKS, revocation, and end-session URLs. At the current version, the authorization endpoint is `/api/auth/oauth2/authorize`, token endpoint is `/api/auth/oauth2/token`, UserInfo endpoint is `/api/auth/oauth2/userinfo`, and keys are at `/api/auth/jwks`, all under `https://account.sudloh.com`. Read discovery rather than hard-coding those paths.
The discovery document gives the actual authorization, token, UserInfo, introspection, JWKS, revocation, and end-session URLs. At the current version, the authorization endpoint is `/api/auth/oauth2/authorize`, token endpoint is `/api/auth/oauth2/token`, UserInfo endpoint is `/api/auth/oauth2/userinfo`, introspection endpoint is `/api/auth/oauth2/introspect`, and keys are at `/api/auth/jwks`, all under `https://account.sudloh.com`. Read discovery rather than hard-coding those paths.
| Endpoint | Method | Caller and purpose |
| --- | --- | --- |
@@ -18,6 +18,7 @@ The discovery document gives the actual authorization, token, UserInfo, JWKS, re
| `/oauth2/token` under the issuer | POST | Server-to-server code exchange with HTTP Basic client authentication |
| `/jwks` under the issuer | GET | Public signing keys for ID-token and JWT verification |
| `/oauth2/userinfo` under the issuer | GET | Bearer access token; returns permitted user claims |
| `/oauth2/introspect` under the issuer | POST | Confidential client authentication; checks its issued access token |
| `/oauth2/revoke` under the issuer | POST | Revokes a token; authenticate the client as advertised by discovery |
| `/oauth2/end-session` under the issuer | GET/POST | Optional OIDC session-end flow for that client |
@@ -30,13 +31,13 @@ For each app, configure these server-side secrets and settings:
| Setting | Value |
| --- | --- |
| `SUDLOH_OIDC_ISSUER` | `https://account.sudloh.com/api/auth` |
| `SUDLOH_OIDC_CLIENT_ID` | Assigned during client registration |
| `SUDLOH_OIDC_CLIENT_SECRET` | Assigned once during client registration; server-side only |
| `SUDLOH_OIDC_CLIENT_ID` | Shown in the admin section when the client is created |
| `SUDLOH_OIDC_CLIENT_SECRET` | Shown once in the admin section when the client is created; server-side only |
| `SUDLOH_OIDC_REDIRECT_URI` | Exact registered HTTPS callback URL on that app |
| scopes | `openid profile email` |
| client authentication | HTTP Basic at token endpoint (`client_secret_basic`) |
Guide and Buzz callback URLs are intentionally not fixed in the account repository. The owner of each app must give its actual callback URL to the account operator before registration. Never use a wildcard callback URL. Keep the client secret out of browser code and logs.
The owner of each app must supply its actual callback URL before registration. Sign in as the verified account administrator, open **Account settings → OIDC clients**, enter the project name and exact HTTPS callback URL, then copy the new ID and secret into that project's server configuration. Never use a wildcard callback URL. Keep the client secret out of browser code and logs. A lost secret requires rotation or a replacement client; the list does not reveal it again.
## Browser sign-in sequence
@@ -53,7 +54,7 @@ The code exchange is a `POST` with `Content-Type: application/x-www-form-urlenco
grant_type=authorization_code&code=<returned-code>&redirect_uri=<exact-registered-uri>&code_verifier=<original-verifier>
```
The successful JSON response contains `access_token`, `token_type`, `expires_in`, `id_token`, and granted `scope`. Do not assume a refresh token: these clients request no `offline_access`. Call UserInfo with `Authorization: Bearer <access_token>` only when fresh claims are needed. The ID token contains standard claims including `iss`, `sub`, `aud`, `exp`, `iat`, and the request's `nonce`; profile and email claims depend on the granted scopes.
The successful JSON response contains `access_token`, `token_type`, `expires_in`, `id_token`, and granted `scope`. Guide's current `openid profile email` grant has no refresh token. The ID token contains standard claims including `iss`, `sub`, `aud`, `exp`, `iat`, and the request's `nonce`. With `profile email`, both a newly issued ID token and UserInfo contain `sub` (stable Sudloh user ID), `name` (string), `picture` (absolute public URL), `email` (string), and `email_verified` (boolean). The picture is a Sudloh default avatar URL if the user has no image. An ID token is immutable; only a newly issued token reflects changes made after issuance.
The provider requires exact redirect URI matching and S256 PKCE. Clients should use a maintained OIDC library for state, nonce, PKCE, token exchange, and ID-token verification.
@@ -85,11 +86,17 @@ Call `authClient.signIn.social({ provider: "sudloh" })` in the client app. Confi
An ID token is for the OAuth client; do not use it to authorize API requests. If an API accepts Sudloh access tokens, validate a JWT against discovery's JWKS and require the expected issuer, expiry, audience/resource, and scope. Register that API as an OAuth protected resource before requesting resource-bound tokens. For the initial Guide and Buzz sign-in integration, use app-owned sessions and do not expose an account access token to the browser API layer.
## Profile updates and sign-out
## Profile updates and active Guide sessions
Email change completes only after the user verifies the new address. Existing app sessions can have older profile claims until their next sign-in or profile refresh. Use `sub` to correlate the user when email changes.
Send users to the stable account settings URL `https://account.sudloh.com/account` to change name, picture, or email. The account page requires a Sudloh sign-in. Picture uploads are stored in the configured Bun S3 bucket and served from its public URL. An email change stays pending until the new address's verification link is opened; only then does UserInfo and a subsequently issued ID token expose the new email. Use `sub` to correlate the user across email changes.
Account sign-out or account-session revocation prevents that account session from initiating future OIDC flows. It does not immediately end Guide or Buzz sessions. Each client app must provide its own sign-out. The provider advertises an OIDC end-session endpoint, but there is no cross-app back-channel logout in this version.
For fresh claims after sign-in, Guide's server calls the discovered `userinfo_endpoint` with `GET` and `Authorization: Bearer <access_token>`. UserInfo reads the current Sudloh user, so the same active access token can return a changed name, picture, or verified email. Require its `sub` to equal the validated ID token's `sub`, and update Guide's cached profile fields. Call after initial sign-in and after the user returns from account settings or otherwise needs a profile refresh. Keep the access token server-side; do not send it to Guide browser code.
Sudloh supports RFC 7662 introspection for Guide's own access token. While a Guide session is in use, check on the first protected request after each five-minute cache interval, including the first request after a quiet period. `POST` to discovery's `introspection_endpoint` with `Content-Type: application/x-www-form-urlencoded`, `Authorization: Basic <base64(client_id:client_secret)>`, and body `token=<access-token>&token_type_hint=access_token`. Require `active: true`, expected `sub`, and `exp` later than the current time. An ended Sudloh Account session makes its issued access token inactive. If the response is inactive, end the Guide session. On an introspection timeout, network failure, malformed response, or server error, deny protected Guide requests until a successful check; never treat a failed check as active. Cache a successful check for no longer than five minutes.
The current access token expires after the issued `expires_in` (normally 3600 seconds). Guide does not request `offline_access`, so it cannot refresh that token. At expiry, restart the authorization-code flow and obtain a new token set before continuing the Guide session. If reauthorization fails or is canceled, end the Guide session. Do not use an ID token or Sudloh browser cookie for UserInfo, introspection, or Guide API authorization.
Account sign-out or account-session revocation makes the bound access token inactive, but it cannot directly delete Guide's independent session. Guide must enforce the checks above and provide its own sign-out. The provider advertises an OIDC end-session endpoint; this integration does not rely on cross-app back-channel logout.
## Errors and checks