Protocols

OpenID Connect

Proof of who the user is, signed and checkable by anyone. Built on the OAuth server.

OpenID Connect adds identity to OAuth. When a client asks for the openid scope, the token reply also carries an id_token: a signed statement saying who the user is. Any client can check it against the public key on /oauth/jwks.

Get an ID token

Authorize exactly like OAuth, but ask for the openid scope. The token reply then includes the ID token alongside the access and refresh tokens.

Login with openid scopets
# 1. Authorize with scope=openid profile email (see OAuth 2.0 server)# 2. Swap the code — the reply now includes id_tokencurl -X POST http://localhost:4000/oauth/token \  -d grant_type=authorization_code \  -d code=CODE -d redirect_uri=myapp://callback \  -d client_id=mobile-app -d code_verifier=VERIFIER # Answer (shortened){  "access_token": "tok_...",  "token_type": "Bearer",  "expires_in": 3600,  "scope": "openid profile email",  "refresh_token": "tok_...",  "id_token": "eyJhbGciOiJSUzI1NiIs..."}

What is inside the ID token

A signed JWT, good for 1 hour. Which personal claims appear depends on the scope that was asked for.

iss
The server. APP_URL, or http://localhost:4000 when that is not set.
sub
The user id.
aud
The client id the token was made for.
exp / iat
Expiry (issued + 1 hour) and issued time.
nonce
Only when a nonce was sent to the authorize URL.
email
Only with the email scope, and only when the user has one.
name
Only with the profile scope, and only when the user has one.

Check it against JWKS

The public key lives on /oauth/jwks. Any client can fetch it and check the ID token signature — no shared secret needed.

Fetch the public keyts
curl http://localhost:4000/oauth/jwks # Answer (shortened){  "keys": [    {      "kty": "RSA",      "kid": "yatta-1",      "alg": "RS256",      "n": "....",      "e": "AQAB"    }  ]}
Warning
ID tokens are signed with RS256. The signing key is made when the server first needs it and does not survive a restart — old ID tokens stop verifying after one. That is fine for short-lived tokens.

Userinfo

The same claims are also available as plain JSON, for a valid access token.

Who is this token for?ts
curl http://localhost:4000/oauth/userinfo \  -H "Authorization: Bearer ACCESS_TOKEN" # Answer — claims follow the scope, like the ID token{  "sub": "user_1",  "email": "ada@example.com",  "name": "Ada"}
401 invalid_token
No token, a bad token, an expired token, or a token with no user (client credentials tokens have none).
403 insufficient_scope
A valid token without the openid scope. A plain API token cannot read profiles here.

Discovery

Clients find every endpoint in one place, with no guessing.

Discovery documentts
curl http://localhost:4000/.well-known/openid-configuration # Answer{  "issuer": "http://localhost:4000",  "authorization_endpoint": "http://localhost:4000/oauth/authorize",  "token_endpoint": "http://localhost:4000/oauth/token",  "userinfo_endpoint": "http://localhost:4000/oauth/userinfo",  "jwks_uri": "http://localhost:4000/oauth/jwks",  "response_types_supported": ["code"],  "subject_types_supported": ["public"],  "id_token_signing_alg_values_supported": ["RS256"],  "scopes_supported": ["openid", "profile", "email"],  "claims_supported": ["sub", "iss", "aud", "exp", "iat", "nonce", "email", "name"],  "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],  "code_challenge_methods_supported": ["S256", "plain"]}