Protocols

OAuth 2.0 server

Yatta as the login server: other apps send their users to you, and you hand back tokens.

Your Yatta app can be the place users sign in. Another app registers as a client, sends its user to /oauth/authorize, and gets back tokens from /oauth/token. Three flows work: authorization code with PKCE, client credentials, and refresh tokens.

Register a client

Clients are listed in one environment variable, as JSON. Public clients (mobile apps, browser apps) leave the secret out — but then they must use PKCE.

OAUTH_CLIENTSts
OAUTH_CLIENTS='[  {    "id": "web-app",    "secret": "a-long-random-secret",    "redirectUris": ["http://localhost:3000/callback"]  },  {    "id": "mobile-app",    "redirectUris": ["myapp://callback"]  }]'

Client fields

id
The client_id. Required.
secret
Optional. Leave it out for a public client.
redirectUris
Allowed redirect URLs. A redirect must match one of these exactly.
name
Optional display name.

Step 1: authorize

Sign in to Yatta first — the user must already be signed in, or this answers 401 login_required. Then send the browser to the authorize URL. On success Yatta redirects back with a code.

Send the user herets
# code_challenge = base64url(sha256(verifier)), method S256http://localhost:4000/oauth/authorize?response_type=code  &client_id=mobile-app  &redirect_uri=myapp://callback  &scope=openid%20profile  &state=abc123  &code_challenge=CHALLENGE&code_challenge_method=S256 # Success redirectmyapp://callback?code=CODE&state=abc123

Authorize parameters

response_type
Only code is supported.
client_id
Must match a registered client.
redirect_uri
Must match a registered redirect URI exactly.
scope
Space-separated list, for example openid profile.
state
Optional. Returned unchanged.
code_challenge
Required for public clients. S256 or plain.
nonce
Optional. Returned inside the ID token when openid scope is asked for.

Step 2: swap the code

POST the code as a form. Prove the client with HTTP Basic auth, or with client_id and client_secret in the form body.

Code for tokensts
curl -X POST http://localhost:4000/oauth/token \  -d grant_type=authorization_code \  -d code=CODE_FROM_STEP_1 \  -d redirect_uri=myapp://callback \  -d client_id=mobile-app \  -d code_verifier=YOUR_VERIFIER # Answer{  "access_token": "tok_...",  "token_type": "Bearer",  "expires_in": 3600,  "scope": "openid profile",  "refresh_token": "tok_..."  // "id_token": "..." — only when openid scope was asked for}

The code works once and lives 10 minutes. Using it twice, or after it expires, answers invalid_grant.

Step 3: refresh

Access tokens live 1 hour. Refresh tokens live 30 days — and they rotate: the old one dies the moment it is used.

Refresh a tokents
curl -X POST http://localhost:4000/oauth/token \  -d grant_type=refresh_token \  -d refresh_token=REFRESH_TOKEN \  -d client_id=mobile-app # Answer: a new pair. The refresh token you just used is dead now.{  "access_token": "tok_...",  "token_type": "Bearer",  "expires_in": 3600,  "scope": "openid profile",  "refresh_token": "tok_..."}
Warning
If a rotated-out refresh token is ever shown again, the whole token family is revoked at once. That is a sign the token may have been stolen.

Client credentials

Services without a user take this flow instead. It needs a client with a secret — a public client answers 401. There is no user here, so there is no refresh token and no ID token.

Service to servicets
curl -X POST http://localhost:4000/oauth/token \  -u web-app:a-long-random-secret \  -d grant_type=client_credentials \  -d scope=read # Answer — access token only{  "access_token": "tok_...",  "token_type": "Bearer",  "expires_in": 3600,  "scope": "read"}

Revoke

Revoking needs valid client auth, like the other token calls. After that it always answers 200 — even for a token that was never real, so nobody can probe which tokens exist.

Revoke a tokents
curl -X POST http://localhost:4000/oauth/revoke \  -u web-app:a-long-random-secret \  -d token=TOKEN_TO_REVOKE# 200, empty body — whether the token existed or not

Lifetimes

Authorization code
10 minutes, one use.
Access token
1 hour.
Refresh token
30 days, rotates on every use.

Errors

invalid_client (401)
Unknown client, or a wrong secret.
invalid_grant (400)
Bad, used, or expired code or refresh token; or a refresh token from another client.
invalid_request (400)
Redirect URI does not match, a public client without PKCE, or an unknown code_challenge_method.
unsupported_response_type (400)
response_type was not code.
unsupported_grant_type (400)
grant_type is not one of the three flows.
login_required (401)
Nobody is signed in to Yatta for the authorize step.
Warning
Client secrets live in the environment, never in code. Codes and tokens live in memory: they do not survive a restart, and each server node holds its own.