SenseCrypt Docs
Concepts

OIDC & OAuth 2.0

SenseCrypt as an OpenID Connect issuer — the Authorization Code flow with PKCE, discovery, PAR, client authentication, grant types, and the token, userinfo, introspection, revocation, and logout endpoints.

SenseCrypt is a standard OpenID Connect provider built on OAuth 2.0. If your app already speaks OIDC, integration is conventional: redirect to authorize, exchange the code, validate the ID token. The biometric ceremony happens between the redirect and the callback and is invisible to your code (see How SenseCrypt works).

Each tenant is its own issuer at its own hostname (see Multi-tenancy). Always resolve endpoints from that tenant's discovery document rather than hard-coding paths.

Discovery

Every tenant publishes an OpenID discovery document and a JWKS:

GET {issuer}/.well-known/openid-configuration
GET {issuer}/.well-known/jwks.json

Read authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, and the other advertised metadata from the discovery document. A representative document looks like this (endpoint URLs are all under the tenant's own issuer host):

{
  "issuer": "https://acme.example.com",
  "authorization_endpoint": "https://acme.example.com/v1/idp/oidc/authorize",
  "token_endpoint": "https://acme.example.com/v1/idp/oidc/token",
  "userinfo_endpoint": "https://acme.example.com/v1/idp/oidc/userinfo",
  "jwks_uri": "https://acme.example.com/.well-known/jwks.json",
  "pushed_authorization_request_endpoint": "https://acme.example.com/v1/idp/oidc/par",
  "require_pushed_authorization_requests": false,
  "revocation_endpoint": "https://acme.example.com/v1/idp/oidc/revoke",
  "introspection_endpoint": "https://acme.example.com/v1/idp/oidc/introspect",
  "end_session_endpoint": "https://acme.example.com/v1/idp/oidc/logout",
  "backchannel_authentication_endpoint": "https://acme.example.com/v1/idp/oidc/bc-authorize",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token", "urn:openid:params:grant-type:ciba"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["ES256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic", "client_secret_jwt", "private_key_jwt"],
  "code_challenge_methods_supported": ["S256"],
  "backchannel_token_delivery_modes_supported": ["poll", "ping", "push"],
  "scopes_supported": ["openid", "profile", "email", "offline_access"],
  "claims_supported": ["sub", "iss", "aud", "exp", "iat", "auth_time", "nonce", "amr", "acr", "..."]
}

A few things worth knowing about the document:

  • scopes_supported and claims_supported are dynamic — they reflect that tenant's enabled scopes and attribute schema. Disable an attribute in the console and it disappears from claims_supported; add a custom scope and it appears in scopes_supported. Read them from the live document; don't assume a fixed list.
  • The JWKS lists the tenant's current signing key plus any key still inside a rotation grace window, so select the verification key by the token's kid and tolerate more than one key.
  • require_pushed_authorization_requests is false — PAR is available but optional (see below).

The Authorization Code flow with PKCE

SenseCrypt supports the Authorization Code flow and requires PKCE with S256:

  1. Your app generates a PKCE code_verifier and its S256 code_challenge, plus a random state, and redirects the browser to the authorization_endpoint.
  2. The user completes the face ceremony on their phone.
  3. The browser returns to your redirect_uri with ?code=...&state=....
  4. Your app exchanges the code (with the code_verifier) at the token_endpoint for tokens.

Here is the full round-trip, including the on-device face ceremony that sits between the redirect (step 2) and the callback (step 3):

redirect to authorization_endpointresponse_type=code, client_id, redirect_uri,scope, state, code_challenge, code_challenge_method=S256 GET /v1/idp/oidc/authorize 302 to enter-email page submit email (creates oauth_sessions row) render QR page (universal link baked into the QR) scan the on-screen QR, then complete the face scan device /complete (records authenticated_at + authorized user) redirect to redirect_uri?code=...&state=... deliver code + state POST /v1/idp/oidc/tokengrant_type=authorization_code, code, code_verifier,client auth if confidential 200 access_token + id_token (ES256) + expires_in + scope + refresh_token? GET /.well-known/jwks.json ES256 public keys (select by kid) validate client + redirect_uri,scope + audience gates, enforce S256 PKCE face proof on device —no biometric data leaves the phone consume single-use code, verify S256(code_verifier),group access gate, max_age check verify id_token signature + claims User Your app (RP) Browser SenseCrypt (tenant issuer) SenseCrypt Authenticator (phone)

A minimal authorize redirect:

GET {authorization_endpoint}
  ?response_type=code
  &client_id={client_id}
  &redirect_uri={your_redirect_uri}
  &scope=openid profile email
  &state={state}
  &code_challenge={code_challenge}
  &code_challenge_method=S256

Key rules — these are enforced, not advisory:

  • PKCE is S256-only. plain is explicitly rejected. A challenge sent with no method is treated as S256. Public/SPA clients are required to use PKCE. The verifier must be 43–128 characters of the RFC 7636 unreserved set (A–Z a–z 0–9 - . _ ~); a verifier presented when the session stored no challenge is a strict reject.
  • response_type is code. SenseCrypt implements the code flow only — there is no implicit or hybrid flow.
  • redirect_uri is validated against your application's registered allow-list (exact match, with trailing-slash tolerance). A mismatch is refused.
  • The authorization code is single-use, short-lived, and bound to your client_id and redirect_uri. It is also bound to the requesting tenant/client before it is consumed, so a cross-tenant or cross-client exchange is refused without burning the code.

The step-by-step version with copy-pasteable requests is in the Add Login (OIDC) quickstart. The authorize endpoint is documented in the API reference.

Forcing re-authentication with max_age

Alongside login_hint, SenseCrypt honors the OIDC max_age parameter at /authorize (and on a PAR push). It is the maximum age, in seconds, of the user's authentication:

GET {authorization_endpoint}
  ?response_type=code
  &client_id={client_id}
  &redirect_uri={your_redirect_uri}
  &scope=openid profile email
  &state={state}
  &code_challenge={code_challenge}
  &code_challenge_method=S256
  &max_age=300

Because SenseCrypt has no server-side SSO cookie, every interactive sign-in is already a fresh on-device face ceremony — so max_age is how you demand a re-authentication (step-up) for a sensitive action rather than accepting a recent session. The value is round-tripped through the ceremony and checked at the token exchange: if the completed face ceremony is older than max_age seconds, the exchange fails with login_required (error codes) and you restart the authorize flow. The resulting token's auth_time records when that ceremony happened.

max_age=0 therefore demands a face ceremony completed essentially at exchange time. Only login_hint and max_age influence the authorize request in this way — SenseCrypt does not read prompt, display, or ui_locales.

Client types and authentication

Your application registers as one of two client types, which determines how (and whether) it authenticates at the token endpoint:

  • Confidential clients (server-side apps) authenticate at the token endpoint. Supported methods are client_secret_post, client_secret_basic, client_secret_jwt, and private_key_jwt — resolve the advertised set from discovery (token_endpoint_auth_methods_supported) and use the method your app is registered for. The method is strictly pinned: presenting a credential over a channel your app isn't registered for is rejected as invalid_client, with no silent downgrade. Client secrets are verified with Argon2id and support a rotation grace window so you can roll a secret without an outage.
  • Public / SPA clients have no secret (the database enforces this) and rely on PKCE. They must present no client credential at all — doing so is rejected. For SPA clients, PKCE and refresh-token rotation are always on.

Unknown client_id, wrong secret, and wrong method all surface as the same opaque invalid_client (401) — you cannot use the error to probe which clients exist.

Pushed Authorization Requests (PAR)

SenseCrypt supports RFC 9126 PAR. Your backend can pre-stage the authorization parameters with a POST to the PAR endpoint and receive a request_uri, then redirect the browser to /authorize with just client_id and request_uri:

POST {pushed_authorization_request_endpoint}
// → 201
{
  "request_uri": "urn:ietf:params:oauth:request_uri:<id>",
  "expires_in": 300,
  "authorize_url": "https://acme.example.com/v1/idp/oidc/authorize"
}

PAR is optional — the default integration is a direct /authorize redirect with a login_hint. Notes:

  • The pushed request_uri is single-use and short-lived (5 minutes by default), and is bound to the client_id that pushed it.
  • When PAR is used, per RFC 9126 the pushed parameters take precedence over any same-named query parameters on the /authorize redirect.
  • Face references never travel through PAR — they are pre-staged server-side on the user record and fetched by the phone during the ceremony.

See the PAR API reference.

Endpoints at a glance

Resolve exact URLs from discovery; the paths below are the conventional shape.

PurposeEndpointAPI reference
DiscoveryGET /.well-known/openid-configurationref
JWKSGET /.well-known/jwks.jsonref
Pushed Authorization RequestPOST /v1/idp/oidc/parref
AuthorizationGET /v1/idp/oidc/authorizeref
TokenPOST /v1/idp/oidc/tokenref
UserInfoGET /v1/idp/oidc/userinforef
Introspection (RFC 7662)POST /v1/idp/oidc/introspectref
Revocation (RFC 7009)POST /v1/idp/oidc/revokeref
RP-Initiated LogoutGET /v1/idp/oidc/logoutref
Backchannel auth (CIBA)POST /v1/idp/oidc/bc-authorizeref

Grant types

The token endpoint accepts:

  • authorization_code — the interactive browser sign-in above.
  • refresh_token — refreshes an access token when your app requested the offline_access scope. See Tokens & sessions.
  • client_credentials — machine-to-machine access for the Management API. See Machine-to-machine.
  • CIBA (urn:openid:params:grant-type:ciba) — decoupled authentication. See CIBA.

Any other grant_type is rejected with unsupported_grant_type. A successful token response is the same shape across the interactive grants:

{
  "access_token": "<jwt>",
  "id_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email",
  "refresh_token": "<opaque>"
}

id_token is present for the user grants; refresh_token appears only when offline_access was granted; expires_in is the access token's lifetime (not the ID token's).

UserInfo, introspection, revocation, logout

  • UserInfo — call GET /userinfo with Authorization: Bearer <access_token> to retrieve the scope-released claims for the user. The endpoint verifies the token against the tenant's JWKS, requires it to be an access token, and honors revocation.
  • Introspection (RFC 7662) — confidential clients may introspect their own tokens. Anything unverifiable, expired, revoked, or belonging to another client returns {"active": false} with a 200 — never an error, so it is not an oracle.
  • Revocation (RFC 7009) — revokes a refresh token's whole family; if the presented token parses as one of your access-token JWTs, its jti is denylisted until it expires. Always returns an empty 200, found or not.
  • RP-Initiated Logout — SenseCrypt has no server-side SSO cookie (each sign-in is a fresh face ceremony), so logout revokes the refresh tokens for the subject. Because there is no session to clear, an id_token_hint is required (a missing hint is invalid_request, not a silent no-op), and any post_logout_redirect_uri must be on your application's dedicated allow-list (distinct from your /authorize redirect list). See Tokens & sessions.
  • Tokens & sessions — token contents, sub, scopes, audiences, and refresh behavior.
  • Authorization — who is allowed to sign in, and the permissions claim.
  • CIBA — backchannel, no-browser authentication.
  • Add Login (OIDC) — the hands-on quickstart.

On this page