Files
buzz-sheet/.agents/skills/sudloh-oidc/references/api.md
T

11 KiB

Sudloh Account OIDC API

Service and identity

  • Production issuer: https://account.sudloh.com/api/auth
  • OIDC discovery: https://account.sudloh.com/api/auth/.well-known/openid-configuration
  • 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. 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, 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
/.well-known/openid-configuration under the issuer GET Public discovery document; fetch before configuring endpoints
/oauth2/authorize under the issuer GET Browser redirect; starts authorization-code sign-in
/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

These are protocol endpoints. Their exact supported methods, authentication methods, and URLs are advertised in discovery; use that document if it differs from this table. Account settings use Better Auth's own same-origin APIs and are not an integration API for Guide or Buzz.

Client configuration

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 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)

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

  1. The app creates a random state, OIDC nonce, and PKCE verifier. Store them in a short-lived, HTTP-only app cookie or server-side transaction store.
  2. Redirect the browser to discovery's authorization_endpoint with response_type=code, assigned client_id, exact redirect_uri, scope=openid profile email, state, nonce, code_challenge, and code_challenge_method=S256.
  3. Sudloh Account redirects the user to Google or Discord if needed and then returns to the registered app callback with code, state, and iss. The account service's session gives subsequent client apps single sign-on.
  4. At the app callback, compare state and iss with the original values and reject errors or missing parameters. Exchange code once at the discovered token_endpoint, authenticating with client ID and secret via HTTP Basic and including the original code_verifier and redirect_uri.
  5. Validate the ID token signature with the discovered JWKS and check iss, aud, exp, and nonce. Read sub, email, name, and picture from validated claims or UserInfo. Create the app's own HTTP-only session.
  6. On later requests, authorize using the app session or a properly validated access token. Do not pass the account cookie between services. Request only the scopes this service needs.

The code exchange is a POST with Content-Type: application/x-www-form-urlencoded, an Authorization: Basic <base64(client_id:client_secret)> header, and a body like:

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. 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.

Better Auth client app example

If Guide or Buzz uses Better Auth, configure its own Better Auth instance and local database with the Generic OAuth plugin:

import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";

export const auth = betterAuth({
  // Configure this app's own database, baseURL, and secret.
  plugins: [genericOAuth({
    config: [{
      providerId: "sudloh",
      clientId: process.env.SUDLOH_OIDC_CLIENT_ID!,
      clientSecret: process.env.SUDLOH_OIDC_CLIENT_SECRET!,
      discoveryUrl: "https://account.sudloh.com/api/auth/.well-known/openid-configuration",
      scopes: ["openid", "profile", "email"],
    }],
  })],
});

Call authClient.signIn.social({ provider: "sudloh" }) in the client app. Confirm that its actual Better Auth callback URL matches the URL registered by the account operator. Its Better Auth instance creates that app's independent session.

Calling a protected API

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 active Guide sessions

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.

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

  • invalid_client: confirm client ID, secret, and registered token auth method.
  • invalid_request or redirect rejection: confirm the exact callback URI, requested scopes, state, and PKCE fields.
  • invalid_grant: code was expired, already used, or exchanged with a different verifier or callback URI. Restart sign-in.
  • access_denied: user canceled the authorization. Return to the app's sign-in screen.
  • Failed signature or issuer/audience/nonce check: discard tokens and fail closed.

Do not log authorization codes, client secrets, tokens, cookie values, or verification links. Use the discovery endpoint and account service /health and /ready endpoints for diagnostics.