{"id":22,"date":"2026-07-15T01:21:57","date_gmt":"2026-07-15T01:21:57","guid":{"rendered":"https:\/\/af4a.com\/?page_id=22"},"modified":"2026-07-15T01:22:06","modified_gmt":"2026-07-15T01:22:06","slug":"partner-integration-example","status":"publish","type":"page","link":"https:\/\/af4a.com\/?page_id=22","title":{"rendered":"Partner Integration Example"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Integrating with AF4A SSO<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This guide is for a partner site that wants to add &#8220;Sign in with AF4A&#8221; \u2014<br>letting users authenticate with their existing AF4A identity instead of<br>creating a new account on your site.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Flow overview<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">AF4A uses the <strong>OAuth 2.0 Authorization Code flow with mandatory PKCE (S256)<\/strong>. There are no client secrets \u2014 PKCE (a one-time cryptographic proof<br>generated by your app) is what proves your app is the same one that started<br>the sign-in, so no secret needs to be stored on your servers.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Your site                             AF4A\n----------                            ----\n1. Generate a code_verifier and derive\n   a code_challenge (S256) from it\n2. Send the user to AF4A to sign in\n   (user logs in \/ registers, then consents)\n                                       3. AF4A issues a one-time\n                                          authorization code\n4. AF4A redirects the user back to your\n   redirect URI with that code attached\n5. Exchange the code (+ code_verifier)\n   for the user's profile\n                                       -&gt; returns { user, connectedApp }\n6. Your site creates its own local session for the user<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Base URL:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;id.af4a.com\/api<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Every endpoint in this guide is reached by appending its path to that base<br>URL \u2014 e.g. <code>https:\/\/id.af4a.com\/api\/oauth\/authorize<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Register as a partner<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before you can use this flow, AF4A needs to register your site as an OAuth<br>client. Contact the AF4A team with:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>App name<\/strong> \u2014 shown to users on the AF4A consent screen.<\/li>\n\n\n\n<li><strong>Domain<\/strong> \u2014 your site&#8217;s domain.<\/li>\n\n\n\n<li><strong>Category<\/strong> \u2014 e.g. Healthcare, Productivity, Community.<\/li>\n\n\n\n<li><strong>Tagline<\/strong> \u2014 one sentence describing your app, shown on the consent screen.<\/li>\n\n\n\n<li><strong>Scopes<\/strong> \u2014 which pieces of profile info you intend to use (e.g.<br><code>profile<\/code>, <code>email<\/code>). This is currently informational\/display-only: every<br>successful sign-in returns the full profile shown below, but request only<br>what you actually use so the consent screen accurately reflects your app.<\/li>\n\n\n\n<li><strong>Redirect URI<\/strong> \u2014 the single, exact URL on your site that AF4A should<br>send users back to after they approve. Authorization codes can only be<br>redeemed against this exact value.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">AF4A will confirm your assigned <code>clientId<\/code> once registered.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Step-by-step integration<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1 \u2014 Generate a PKCE pair<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Before sending the user to AF4A, generate a <code>code_verifier<\/code> (a random<br>secret) and derive its <code>code_challenge<\/code> (a SHA-256 hash of that secret,<br>base64url-encoded). Keep the verifier only in memory or short-lived<br>client-side storage on the browser tab that starts the flow \u2014 never put it<br>in a URL.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function base64UrlEncode(bytes: Uint8Array): string {\n  let binary = \"\";\n  for (const byte of bytes) binary += String.fromCharCode(byte);\n  return btoa(binary).replace(\/\\+\/g, \"-\").replace(\/\\\/\/g, \"_\").replace(\/=+$\/, \"\");\n}\n\nfunction generateCodeVerifier(): string {\n  const bytes = new Uint8Array(32);\n  crypto.getRandomValues(bytes);\n  return base64UrlEncode(bytes);\n}\n\nasync function deriveCodeChallenge(verifier: string): Promise&lt;string&gt; {\n  const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n  return base64UrlEncode(new Uint8Array(digest));\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Stash the verifier (e.g. in <code>sessionStorage<\/code>, keyed by your <code>clientId<\/code>) so<br>you can retrieve it once the user is redirected back to your callback page.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2 \u2014 Send the user to AF4A and request authorization<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The user first signs in (or registers) with their AF4A account, then<br>reviews and approves a consent screen naming your app and the information<br>it will access. Once they approve, request an authorization code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>POST \/oauth\/authorize\nAuthorization: Bearer &lt;the user's AF4A session token&gt;\nContent-Type: application\/json\n\n{\n  \"clientId\": \"your-client-id\",\n  \"redirectUri\": \"https:\/\/yoursite.example\/callback\",\n  \"codeChallenge\": \"&lt;base64url SHA-256 of your code_verifier&gt;\",\n  \"codeChallengeMethod\": \"S256\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Response (<code>201<\/code>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{ \"code\": \"a1b2c3...\" }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Possible error responses (<code>400<\/code>\/<code>401<\/code>\/<code>404<\/code>, body <code>{ \"error\": \"...\" }<\/code>):<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>User not signed in to AF4A.<\/li>\n\n\n\n<li>Unknown or unregistered <code>clientId<\/code>.<\/li>\n\n\n\n<li><code>redirectUri<\/code> doesn&#8217;t exactly match the one AF4A has on file for your app.<\/li>\n\n\n\n<li>Missing or invalid PKCE challenge \u2014 <code>codeChallengeMethod<\/code> must be <code>\"S256\"<\/code>.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The returned <code>code<\/code> is single-use and expires after <strong>5 minutes<\/strong>. Once you<br>have it, redirect the user&#8217;s browser to your own redirect URI with the code<br>attached, e.g. <code>https:\/\/yoursite.example\/callback?code=&lt;code&gt;<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3 \u2014 Exchange the code for the user&#8217;s profile<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On your callback page, read <code>code<\/code> from the URL, retrieve the <code>code_verifier<\/code><br>you stashed in Step 1, and exchange them for the user&#8217;s profile:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>POST \/oauth\/token\nContent-Type: application\/json\n\n{\n  \"code\": \"a1b2c3...\",\n  \"clientId\": \"your-client-id\",\n  \"redirectUri\": \"https:\/\/yoursite.example\/callback\",\n  \"codeVerifier\": \"&lt;the original code_verifier from Step 1&gt;\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Response (<code>200<\/code>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"user\": {\n    \"id\": 4,\n    \"name\": \"Jordan Rivera\",\n    \"email\": \"jordan@example.com\",\n    \"createdAt\": \"2026-06-01T12:00:00.000Z\",\n    \"textScale\": \"standard\",\n    \"highContrast\": false,\n    \"reduceMotion\": false,\n    \"screenReaderHints\": false,\n    \"voiceControlEnabled\": false\n  },\n  \"connectedApp\": {\n    \"id\": 12,\n    \"userId\": 4,\n    \"name\": \"Your App Name\",\n    \"domain\": \"yoursite.example\",\n    \"category\": \"Productivity\",\n    \"scopes\": &#91;\"profile\", \"email\"],\n    \"connectedAt\": \"2026-07-01T09:00:00.000Z\",\n    \"lastUsedAt\": \"2026-07-15T10:00:00.000Z\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>user<\/code> object includes the person&#8217;s AF4A accessibility preferences<br>(<code>textScale<\/code>, <code>highContrast<\/code>, <code>reduceMotion<\/code>, <code>screenReaderHints<\/code>,<br><code>voiceControlEnabled<\/code>). Partner sites are encouraged to honor these so a<br>user&#8217;s accessibility settings follow them across every site they sign into<br>with AF4A, not just AF4A itself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Possible error responses (<code>400<\/code>, body <code>{ \"error\": \"...\" }<\/code>):<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Unknown <code>clientId<\/code>.<\/li>\n\n\n\n<li><code>redirectUri<\/code> doesn&#8217;t match the one used in Step 2.<\/li>\n\n\n\n<li>Code is invalid, expired, already used, or the <code>codeVerifier<\/code> doesn&#8217;t<br>match what was presented in Step 2 \u2014 all of these return a generic<br>&#8220;Invalid or expired code&#8221; so a failed attempt doesn&#8217;t reveal which check<br>failed.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This endpoint is rate-limited (per IP and per client), so build in<br>reasonable retry\/backoff and avoid retrying automatically in a loop if<br>something goes wrong.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4 \u2014 Establish your own session<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AF4A does not issue an ongoing access token for your site to call AF4A APIs<br>later \u2014 this is a one-time identity handoff. Once you have the <code>user<\/code><br>object, create your own local session (cookie, JWT, or whatever your site<br>already uses) and the AF4A part of the flow is complete. If you need to<br>confirm the user&#8217;s identity again later, simply run the flow again.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Discovering AF4A partner listings<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>GET \/oauth\/clients<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Returns the public list of AF4A-registered partner apps (<code>clientId<\/code>,<br><code>name<\/code>, <code>domain<\/code>, <code>category<\/code>, <code>tagline<\/code>, <code>scopes<\/code>, <code>redirectUri<\/code>). This is<br>unauthenticated and is mainly useful if you&#8217;re building a directory or<br>&#8220;available partners&#8221; page; an already-integrated partner doesn&#8217;t need it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Security notes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>There is no client secret and no server-to-server credential for this<br>flow \u2014 security relies entirely on PKCE plus the exact-match redirect<br>URI. Always use PKCE and never reuse a <code>code_verifier<\/code> across attempts.<\/li>\n\n\n\n<li>AF4A user sessions use a 7-day sliding idle timeout, capped at a 30-day<br>absolute lifetime. In practice this just means a user may occasionally<br>need to sign back in to AF4A before starting your flow.<\/li>\n\n\n\n<li>Authorization codes are single-use and expire after 5 minutes \u2014 don&#8217;t<br>cache or retry them.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Integrating with AF4A SSO This guide is for a partner site that wants to add &#8220;Sign in with AF4A&#8221; \u2014letting users authenticate with their existing AF4A identity instead ofcreating 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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-22","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/af4a.com\/index.php?rest_route=\/wp\/v2\/pages\/22","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/af4a.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/af4a.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/af4a.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/af4a.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=22"}],"version-history":[{"count":2,"href":"https:\/\/af4a.com\/index.php?rest_route=\/wp\/v2\/pages\/22\/revisions"}],"predecessor-version":[{"id":24,"href":"https:\/\/af4a.com\/index.php?rest_route=\/wp\/v2\/pages\/22\/revisions\/24"}],"wp:attachment":[{"href":"https:\/\/af4a.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=22"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}