Last1 ID OAuth2/OIDC Integration (v1)¶
Last1 ID supports OAuth 2.0 Authorization Code flow with PKCE as the primary standard for cross-platform login and consented API access.
Canonical Issuer¶
- Production issuer:
https://auth.last1.id - Discovery document:
https://auth.last1.id/.well-known/openid-configuration - JWKS:
https://auth.last1.id/.well-known/jwks.json - Do not use
https://last1.idas the issuer. That host is marketing HTML, not OIDC JSON.
Endpoints¶
- Authorization:
GET /oauth/authorize - Token:
POST /oauth/token - OIDC discovery:
GET /.well-known/openid-configuration - JWKS:
GET /.well-known/jwks.json - Consent validation helper:
GET /api/oauth/authorize/validate - Consent approval helper (signed-in user):
POST /api/oauth/authorize/approve
Creating Client Credentials (Org Admin)¶
- Open
App clientsin the Last1 ID admin portal. - Click
Register app, fill app ID + name, then submit. - Save:
client_id: click Copy Client ID (the row UUID). The App ID slug is also accepted on/oauth/authorize.client_secret(shown once in registration success modal)- If
client_secretis lost, clickRegenerate client secreton the app row and update your deployment secret store.
Required Authorization Request Params¶
response_type=codeclient_idredirect_uri(must exactly match registered URI)scope(space-delimited)statecode_challenge(PKCE S256 required for public clients)code_challenge_method=S256
Optional:
- nonce (recommended; required when requesting openid)
Token Request¶
POST /oauth/token
Supported grant types:
- authorization_code
- refresh_token
For authorization_code:
- grant_type=authorization_code
- code
- redirect_uri
- client_id
- code_verifier (required for PKCE/public clients)
For refresh_token:
- grant_type=refresh_token
- refresh_token
- client_id
Client authentication:
- Confidential clients: client_secret_basic or client_secret_post
- Public clients: none + PKCE
Token Response¶
access_tokentoken_type(Bearer)expires_inscoperefresh_tokenrefresh_expires_inid_token(included whenopenidscope granted)
Supported Scopes¶
openidprofileoffline_accessidentity:readcredentials:readcredentials:writecredentials:issuecredentials:verifyapp_links:readtrust_events:readtrust_events:write
Security Requirements¶
- Exact redirect URI matching (no wildcard redirects)
- PKCE (
S256) for public clients - Single-use short-lived authorization codes
- Rotating refresh tokens
- Token exchange audit logging
- Per-IP rate limiting on OAuth endpoints
- Discovery
issuermust exactly match configuredLAST1_ISSUER_URL - JWKS must contain at least one signing key (RS256 expected in production)
Identity Linkage Contract¶
After a successful /oauth/token exchange, the partner receives:
id_token— JWT whosesubclaim is the user's last1.id identity UUID. This is the canonical key the partner should store on its user record (e.g.veterans.last1_identity_idon LastVet,users.last1_identity_idon RadioCheck).access_token— JWT for future calls into scoped last1.id APIs. (Access-token validation on resource routes is a future enhancement; today theid_tokencarries everything a partner needs to link.)
On the last1.id side, the token endpoint upserts a row into identity_app_links keyed on (identity_id, app_id). Per the post-2026-05 contract, identity_app_links.app_user_id is intentionally NULL after an OAuth login. The OAuth authorization-code flow does not carry the partner's internal user id, and writing the last1.id identity UUID into app_user_id (as earlier versions did) corrupted the column's semantic meaning.
If a partner wants app_user_id populated for bidirectional discoverability on the last1.id consent dashboard, they should call the (future) POST /api/app-links/register endpoint with their app secret and their internal user id after creating the local user record from id_token.sub. Most partners do not need this — keeping the link in their own DB via last1_identity_id is sufficient.
The legacy POST /api/identity/authorize/complete flow is dual-run and being retired. RadioCheck and MATR still use /authorize?app=... until they ship PKCE. New integrations must not start on the bridge. After both partners ship PKCE, /authorize?app= and authorize/complete retire.
Sign in with Last1 ID — End-to-End¶
For a partner with a backend (e.g. last.vet-provider talking to last.vet-api):
- Frontend generates a PKCE pair, stores
code_verifierin session storage, and redirects to:
https://auth.last1.id/oauth/authorize
?response_type=code
&client_id=<Copy-Client-ID-UUID-or-app-id-slug>
&redirect_uri=<your-registered-callback>
&scope=openid+email+profile+offline_access+identity:read
&state=<csrf-token>
&nonce=<one-time-nonce>
&code_challenge=<base64url-sha256(verifier)>
&code_challenge_method=S256
- last1.id authenticates the user (Supabase Auth), shows the consent screen, and on Allow redirects to:
-
Frontend posts
{ code, code_verifier, state }to its own backend (do not exchange the code in the browser for confidential clients). -
Backend exchanges the code:
POST https://auth.last1.id/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<auth-code>
&redirect_uri=<your-registered-callback>
&client_id=<Copy-Client-ID-UUID-or-app-id-slug>
&client_secret=<your-app-secret> # confidential clients
&code_verifier=<original-verifier>
- Backend verifies the returned
id_tokenJWT againsthttps://auth.last1.id/.well-known/jwks.json, assertsiss,aud=<client_id>,nonce, and a recentiat. Then upserts the local user record keyed onid_token.suband issues a local session.
Backward Compatibility¶
The /authorize?app= + POST /api/identity/authorize/complete bridge is dual-run only and is being retired. Do not start new integrations on it. Use Authorization Code + PKCE.