Developer Documentation

Everything you need to integrate with this OAuth 2.0 and OpenID Connect (OIDC) authorization server — from registering your application to authenticating users and fetching profile data.

How It Works

The server implements the standard Authorization Code Grant flow with PKCE, separating authentication (verifying identity) from authorization (granting permissions).

  1. Authorization Request: Your app redirects the user to this server.
  2. Authentication: The user logs in securely on this domain.
  3. Consent: The user reviews the permissions (scopes) your app is requesting and approves them.
  4. Callback: The server redirects the user back to your app with a short-lived authorization code.
  5. Token Exchange: Your backend exchanges the code for an Access Token and ID Token.
  6. Data Access: Your app uses the Access Token to fetch user profile data.
Your App                  Auth Server               User
   |                           |                      |
   |-- GET /o/authorization -->|                      |
   |   (client_id, scope,      |                      |
   |    state, code_challenge) |                      |
   |                           |<-- login/register ---|
   |                           |--- consent screen -->|
   |                           |<-- user approves ----|
   |<-- redirect ?code=... ----|                      |
   |                           |                      |
   |-- POST /o/token ----------|                      |
   |   (code, code_verifier,   |                      |
   |    client_secret)         |                      |
   |<-- access_token, id_token-|                      |
   |                           |                      |
   |-- GET /o/userinfo ------->|                      |
   |   (Bearer access_token)   |                      |
   |<-- { sub, name, email } --|                      |

Authentication Flow

Users interact with this server directly to register and log in. Your app never handles passwords — it only receives tokens after the user authenticates here.

New users

When a user hits /o/authorization without an active session, they are redirected to the sign-up/login page. After creating an account and verifying their identity, they are returned to the consent screen and then back to your app.

Returning users

If the user already has a valid session cookie on this domain, the login step is skipped. If they have previously granted the same scopes to your client, the consent screen is also skipped and they are redirected straight back with a new code.

Consent grants are stored per user_id + client_id pair in the oauth_consents table. Requesting additional scopes later will re-show the consent screen for only the new scopes.

Client Registration

Before integrating, register your application in the Developer Portal to obtain credentials.

  1. Go to the Developer Portal and sign in.
  2. Click Register a new application.
  3. Provide your Application Name, Application URL, and Redirect URI — the exact endpoint in your app that will handle the authorization callback.
  4. You will receive a Client ID and a Client Secret.
Keep your Client Secret secure. Never expose it in client-side code, frontend JavaScript, or mobile apps.

Generating PKCE

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. You generate a random code_verifier, hash it to produce the code_challenge, and send the challenge with the authorization request. The verifier is sent only during the token exchange.

// Works in any modern browser or Node.js 20+
async function generatePKCE() {
  // 1. Random 32-byte verifier, base64url-encoded
  const array = crypto.getRandomValues(new Uint8Array(32));
  const codeVerifier = btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

  // 2. SHA-256 hash of the verifier
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const digest = await crypto.subtle.digest('SHA-256', data);

  // 3. base64url-encode the hash → code_challenge
  const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

  return { codeVerifier, codeChallenge };
}

// Generate a random state to prevent CSRF
const state = crypto.randomUUID();

Store codeVerifier and state in sessionStorage before redirecting. Verify state matches when your callback receives the redirect.

Integration Guide

Step 1 — Request Authorization

Redirect the user's browser to the authorization endpoint. Use the PKCE values and state generated above. The state parameter is an opaque random string your app generates — the server echoes it back in the callback so you can verify the redirect wasn't forged (CSRF protection).

GET /o/authorization?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://yourapp.com/callback&
  response_type=code&
  scope=openid email profile&
  state=YOUR_RANDOM_STATE&
  code_challenge=YOUR_PKCE_CHALLENGE&
  code_challenge_method=S256

On callback, verify state matches what you stored before proceeding with the code exchange.

Step 2 — Exchange Code for Tokens

After the user approves, they are redirected to your redirect_uri with a code. Exchange it from your backend:

POST /o/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "code": "CODE_FROM_CALLBACK",
  "redirect_uri": "https://yourapp.com/callback",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "code_verifier": "YOUR_PKCE_VERIFIER"
}

The response includes an access_token, id_token, and optionally a refresh_token.

{
  "access_token": "eyJhbGciOiJSUzI1NiJ9...",
  "id_token": "eyJhbGciOiJSUzI1NiJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "opaque-refresh-token-string"
}

refresh_token is only present when offline_access was in the requested scope.

Step 3 — Fetch User Data

Use the Access Token to retrieve the authenticated user's profile:

GET /o/userinfo
Authorization: Bearer YOUR_ACCESS_TOKEN
{
  "sub": "3f2a1b4c-...",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "picture": "https://example.com/avatar.jpg"
}

Step 4 — Refresh Tokens

Access tokens expire after 1 hour. If you requested the offline_access scope, use the Refresh Token to get a new Access Token without prompting the user again:

POST /o/token
Content-Type: application/json

{
  "grant_type": "refresh_token",
  "refresh_token": "YOUR_REFRESH_TOKEN",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET"
}

Endpoints Reference

Endpoint Method Description
/.well-known/openid-configuration GET OIDC Discovery document — all endpoints and supported configurations.
/o/certs GET JSON Web Key Set (JWKS) used to verify JWT signatures.
/o/authorization GET Authorization endpoint — users log in and grant consent here.
/o/token POST Exchanges authorization codes or refresh tokens for active tokens.
/o/userinfo GET Returns user profile data. Requires a valid Bearer token.

Scopes & Tokens

Supported Scopes

  • openid — Required for OIDC. Verifies the user's identity.
  • email — Access the user's email address.
  • profile — Access the user's name and profile picture.
  • offline_access — Issues a Refresh Token for long-lived access.

Token Types

All JWTs are signed with RS256. Verify their authenticity using the public keys at /o/certs.
  • Access Token — A stateless JWT valid for 1 hour. Contains client_id, sub (user ID), and granted scopes. For first-party apps, also set as an HttpOnly secure cookie.
  • ID Token — An OIDC-compliant JWT with identity claims like email and name.
  • Refresh Token — An opaque string with a longer lifetime. Store it securely in your backend only.