Partner Integration Example

Integrating with AF4A SSO

This guide is for a partner site that wants to add “Sign in with AF4A” —
letting users authenticate with their existing AF4A identity instead of
creating a new account on your site.

1. Flow overview

AF4A uses the OAuth 2.0 Authorization Code flow with mandatory PKCE (S256). There are no client secrets — PKCE (a one-time cryptographic proof
generated by your app) is what proves your app is the same one that started
the sign-in, so no secret needs to be stored on your servers.

Your site                             AF4A
----------                            ----
1. Generate a code_verifier and derive
   a code_challenge (S256) from it
2. Send the user to AF4A to sign in
   (user logs in / registers, then consents)
                                       3. AF4A issues a one-time
                                          authorization code
4. AF4A redirects the user back to your
   redirect URI with that code attached
5. Exchange the code (+ code_verifier)
   for the user's profile
                                       -> returns { user, connectedApp }
6. Your site creates its own local session for the user

Base URL:

https://id.af4a.com/api

Every endpoint in this guide is reached by appending its path to that base
URL — e.g. https://id.af4a.com/api/oauth/authorize.

2. Register as a partner

Before you can use this flow, AF4A needs to register your site as an OAuth
client. Contact the AF4A team with:

  • App name — shown to users on the AF4A consent screen.
  • Domain — your site’s domain.
  • Category — e.g. Healthcare, Productivity, Community.
  • Tagline — one sentence describing your app, shown on the consent screen.
  • Scopes — which pieces of profile info you intend to use (e.g.
    profile, email). This is currently informational/display-only: every
    successful sign-in returns the full profile shown below, but request only
    what you actually use so the consent screen accurately reflects your app.
  • Redirect URI — the single, exact URL on your site that AF4A should
    send users back to after they approve. Authorization codes can only be
    redeemed against this exact value.

AF4A will confirm your assigned clientId once registered.

3. Step-by-step integration

Step 1 — Generate a PKCE pair

Before sending the user to AF4A, generate a code_verifier (a random
secret) and derive its code_challenge (a SHA-256 hash of that secret,
base64url-encoded). Keep the verifier only in memory or short-lived
client-side storage on the browser tab that starts the flow — never put it
in a URL.

function base64UrlEncode(bytes: Uint8Array): string {
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

function generateCodeVerifier(): string {
  const bytes = new Uint8Array(32);
  crypto.getRandomValues(bytes);
  return base64UrlEncode(bytes);
}

async function deriveCodeChallenge(verifier: string): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
  return base64UrlEncode(new Uint8Array(digest));
}

Stash the verifier (e.g. in sessionStorage, keyed by your clientId) so
you can retrieve it once the user is redirected back to your callback page.

Step 2 — Send the user to AF4A and request authorization

The user first signs in (or registers) with their AF4A account, then
reviews and approves a consent screen naming your app and the information
it will access. Once they approve, request an authorization code:

POST /oauth/authorize
Authorization: Bearer <the user's AF4A session token>
Content-Type: application/json

{
  "clientId": "your-client-id",
  "redirectUri": "https://yoursite.example/callback",
  "codeChallenge": "<base64url SHA-256 of your code_verifier>",
  "codeChallengeMethod": "S256"
}

Response (201):

{ "code": "a1b2c3..." }

Possible error responses (400/401/404, body { "error": "..." }):

  • User not signed in to AF4A.
  • Unknown or unregistered clientId.
  • redirectUri doesn’t exactly match the one AF4A has on file for your app.
  • Missing or invalid PKCE challenge — codeChallengeMethod must be "S256".

The returned code is single-use and expires after 5 minutes. Once you
have it, redirect the user’s browser to your own redirect URI with the code
attached, e.g. https://yoursite.example/callback?code=<code>.

Step 3 — Exchange the code for the user’s profile

On your callback page, read code from the URL, retrieve the code_verifier
you stashed in Step 1, and exchange them for the user’s profile:

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

{
  "code": "a1b2c3...",
  "clientId": "your-client-id",
  "redirectUri": "https://yoursite.example/callback",
  "codeVerifier": "<the original code_verifier from Step 1>"
}

Response (200):

{
  "user": {
    "id": 4,
    "name": "Jordan Rivera",
    "email": "jordan@example.com",
    "createdAt": "2026-06-01T12:00:00.000Z",
    "textScale": "standard",
    "highContrast": false,
    "reduceMotion": false,
    "screenReaderHints": false,
    "voiceControlEnabled": false
  },
  "connectedApp": {
    "id": 12,
    "userId": 4,
    "name": "Your App Name",
    "domain": "yoursite.example",
    "category": "Productivity",
    "scopes": ["profile", "email"],
    "connectedAt": "2026-07-01T09:00:00.000Z",
    "lastUsedAt": "2026-07-15T10:00:00.000Z"
  }
}

The user object includes the person’s AF4A accessibility preferences
(textScale, highContrast, reduceMotion, screenReaderHints,
voiceControlEnabled). Partner sites are encouraged to honor these so a
user’s accessibility settings follow them across every site they sign into
with AF4A, not just AF4A itself.

Possible error responses (400, body { "error": "..." }):

  • Unknown clientId.
  • redirectUri doesn’t match the one used in Step 2.
  • Code is invalid, expired, already used, or the codeVerifier doesn’t
    match what was presented in Step 2 — all of these return a generic
    “Invalid or expired code” so a failed attempt doesn’t reveal which check
    failed.

This endpoint is rate-limited (per IP and per client), so build in
reasonable retry/backoff and avoid retrying automatically in a loop if
something goes wrong.

Step 4 — Establish your own session

AF4A does not issue an ongoing access token for your site to call AF4A APIs
later — this is a one-time identity handoff. Once you have the user
object, create your own local session (cookie, JWT, or whatever your site
already uses) and the AF4A part of the flow is complete. If you need to
confirm the user’s identity again later, simply run the flow again.

4. Discovering AF4A partner listings

GET /oauth/clients

Returns the public list of AF4A-registered partner apps (clientId,
name, domain, category, tagline, scopes, redirectUri). This is
unauthenticated and is mainly useful if you’re building a directory or
“available partners” page; an already-integrated partner doesn’t need it.

5. Security notes

  • There is no client secret and no server-to-server credential for this
    flow — security relies entirely on PKCE plus the exact-match redirect
    URI. Always use PKCE and never reuse a code_verifier across attempts.
  • AF4A user sessions use a 7-day sliding idle timeout, capped at a 30-day
    absolute lifetime. In practice this just means a user may occasionally
    need to sign back in to AF4A before starting your flow.
  • Authorization codes are single-use and expire after 5 minutes — don’t
    cache or retry them.