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_CLIENTS='[ { "id": "web-app", "secret": "a-long-random-secret", "redirectUris": ["http://localhost:3000/callback"] }, { "id": "mobile-app", "redirectUris": ["myapp://callback"] }]'Client fields
idsecretredirectUrisnameStep 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.
# 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=abc123Authorize parameters
response_typeclient_idredirect_uriscopestatecode_challengenonceStep 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.
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.
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_..."}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.
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.
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 notLifetimes
Authorization codeAccess tokenRefresh tokenErrors
invalid_client (401)invalid_grant (400)invalid_request (400)unsupported_response_type (400)unsupported_grant_type (400)login_required (401)