Developer guide · OpenID Connect

Add CodeB sign-on to your app.

This page walks you through everything you need to consume CodeB’s OpenID Connect identity provider from a third-party website — PKCE setup, the auth code exchange, JWT validation, refresh handling, and logout. Copy-paste samples in vanilla JavaScript and Node, plus a curl walkthrough for backend tinkering.

The whole IdP is standards-compliant OIDC Core 1.0. If you already use a library like oidc-client-ts, openid-client (Node), Authlib (Python), or .NET’s built-in OpenIdConnectAuthentication — point it at the discovery URL and configure your client ID. Most of the manual steps below collapse to a config block.

Contents
  1. Prerequisites
  2. Register your application
  3. Discover the endpoints
  4. The sign-in flow
  5. Full code — vanilla JS
  6. Full code — Node / Express
  7. curl walkthrough
  8. Validate the ID token
  9. Refresh + logout
  10. Token revocation
  11. Authentication factor (amr / acr)
  12. Key rotation runbook
  13. Troubleshooting
  14. Quick reference

1. Prerequisites

2. Register your application

CodeB requires every relying party to be pre-registered — this is the standard OIDC posture (RFC 6749 §3.1.2.1). Pre-registration locks down which redirect_uri values the IdP will honour, which is what prevents an attacker from intercepting your users’ auth codes.

The operator adds your client to App_Data/<tenant>/oidc/clients.json. The file is per-tenant and hot-reloaded on every change — no IIS recycle required. Example:

{ "clients": [ { "client_id": "nextcloud", "redirect_uris": ["https://cloud.example.com/index.php/apps/user_oidc/code"], "description": "Acme Nextcloud (user_oidc app)" }, { "client_id": "acme-portal", "redirect_uris": [ "https://app.acme.com/oauth/callback", "https://staging.acme.com/oauth/callback" ], "description": "Acme support portal (prod + staging)" } ] }

If you don’t operate the host, email info@codeb.io with your application name, the exact callback URL (HTTPS unless loopback), and whether you need test vs. production redirects. All CodeB clients are public PKCE-only — no client secret to manage.

The built-in codeb-admin client is always available and bound to https://<tenant>/oidc-callback.html — that’s what powers the admin UI. You can’t override or remove it from clients.json.

For experimentation against your own tenant, the built-in client codeb-admin is pre-registered with the redirect URI https://<your-host>/oidc-callback.html — that’s what the CodeB admin pages use. Don’t use it for production third-party integrations; request your own client ID.

3. Discover the endpoints

The IdP publishes everything you need at the standard well-known URL:

curl https://phone.codeb.io/.well-known/openid-configuration

Response (abridged):

{ "issuer": "https://phone.codeb.io", "authorization_endpoint": "https://phone.codeb.io/oidc.ashx?action=authorize", "token_endpoint": "https://phone.codeb.io/oidc.ashx?action=token", "userinfo_endpoint": "https://phone.codeb.io/oidc.ashx?action=userinfo", "jwks_uri": "https://phone.codeb.io/.well-known/jwks.json", "end_session_endpoint": "https://phone.codeb.io/oidc.ashx?action=end_session", "response_types_supported": ["code"], "id_token_signing_alg_values_supported": ["RS256"], "code_challenge_methods_supported": ["S256"], "grant_types_supported": ["authorization_code", "refresh_token"], "scopes_supported": ["openid", "profile", "email", "groups", "phone", "address"], "token_endpoint_auth_methods_supported": ["none"] }

Most libraries fetch this once at startup and cache it. Re-fetch if you start seeing kid mismatches on token validation (operator rotated the key).

4. The sign-in flow

Standard Authorization Code with PKCE. Four moving parts: your front-end, your back-end, the user’s browser, and the CodeB IdP.

1

Generate a PKCE pair on the front-end

Random 32-byte code_verifier, base64url-encoded. code_challenge is the SHA-256 of the verifier, also base64url. Stash the verifier in sessionStorage for the callback to find.

2

Redirect the user to /authorize

With your client_id, your redirect_uri, requested scopes (always include openid), a random state token, the code_challenge, and code_challenge_method=S256.

3

CodeB prompts the user to sign in

The user lands on /login.html, types their CodeB username + password (hashed client-side to HA1; the plaintext password never leaves the browser). On success, CodeB redirects back to your redirect_uri with ?code=…&state=….

4

Validate the state, exchange the code

Front-end checks that state matches what it sent. Then either the front-end (public client) or the back-end (confidential client) POSTs the code + PKCE verifier to /token and receives access_token, id_token, refresh_token.

5

Verify the ID token, sign the user in

Validate the JWT signature against the IdP’s JWKS, check iss, aud, exp, nonce. The sub claim is the user’s ID; role is admin/user/siponly/guest.

5. Full code — vanilla JS (public SPA client)

For a single-page app that does the whole flow client-side. Two files: a launcher and a callback handler.

Launcher (the button that starts sign-in)

// On the page that has your "Sign in with CodeB" button: async function signInWithCodeB() { // 1. Generate PKCE verifier + challenge const verifier = b64url(crypto.getRandomValues(new Uint8Array(48))); const challenge = b64url(new Uint8Array( await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)))); const state = b64url(crypto.getRandomValues(new Uint8Array(16))); // 2. Stash them so the callback page can retrieve them sessionStorage.setItem('oidc.verifier', verifier); sessionStorage.setItem('oidc.state', state); // 3. Redirect to /authorize const params = new URLSearchParams({ response_type: 'code', client_id: 'YOUR_CLIENT_ID', redirect_uri: 'https://your-app.com/oidc/callback', scope: 'openid profile email', state, code_challenge: challenge, code_challenge_method: 'S256' }); location.assign('https://phone.codeb.io/oidc.ashx?action=authorize&' + params); } function b64url(bytes) { return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }

Callback handler (the page CodeB redirects back to)

// At https://your-app.com/oidc/callback (async () => { const url = new URL(location.href); const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const saved = sessionStorage.getItem('oidc.state'); const verif = sessionStorage.getItem('oidc.verifier'); // CRITICAL: enforce state. Missing OR mismatched -> abort. if (!code || !saved || state !== saved || !verif) { document.body.textContent = 'Sign-in failed (bad state).'; return; } sessionStorage.removeItem('oidc.state'); sessionStorage.removeItem('oidc.verifier'); // Exchange the code for tokens const body = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: 'https://your-app.com/oidc/callback', client_id: 'YOUR_CLIENT_ID', code_verifier: verif }); const r = await fetch('https://phone.codeb.io/oidc.ashx?action=token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body }); const tok = await r.json(); if (!r.ok || !tok.access_token) { document.body.textContent = 'Token exchange failed: ' + (tok.error_description || tok.error); return; } // You now have: // tok.access_token -- send as Authorization: Bearer to your API // tok.id_token -- JWT identifying the user (sub, role, profile claims) // tok.refresh_token -- exchange for a fresh access_token later // tok.expires_in -- seconds until access_token expires (typically 3600) sessionStorage.setItem('app.access_token', tok.access_token); sessionStorage.setItem('app.refresh_token', tok.refresh_token); sessionStorage.setItem('app.expires_at', Math.floor(Date.now()/1000) + tok.expires_in); location.replace('/'); })();

Public clients must use PKCE. Without it, anyone who intercepts the redirect URL can exchange the code for tokens. We don’t accept the authorization_code grant without a code_verifier.

6. Full code — Node / Express (confidential client)

For a backend that does the token exchange server-side — the recommended pattern when you control your own server. Uses the openid-client library, which handles discovery, PKCE, token validation, and refresh.

// npm install openid-client express express-session import express from 'express'; import session from 'express-session'; import { Issuer, generators } from 'openid-client'; const app = express(); app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true })); const codeb = await Issuer.discover('https://phone.codeb.io'); const client = new codeb.Client({ client_id: process.env.CODEB_CLIENT_ID, client_secret: process.env.CODEB_CLIENT_SECRET, // only for confidential clients redirect_uris: ['https://your-app.com/oidc/callback'], response_types: ['code'] }); app.get('/login', (req, res) => { const verifier = generators.codeVerifier(); const state = generators.state(); req.session.pkce = verifier; req.session.state = state; res.redirect(client.authorizationUrl({ scope: 'openid profile email', code_challenge: generators.codeChallenge(verifier), code_challenge_method: 'S256', state })); }); app.get('/oidc/callback', async (req, res) => { const params = client.callbackParams(req); const tokenSet = await client.callback( 'https://your-app.com/oidc/callback', params, { code_verifier: req.session.pkce, state: req.session.state } ); req.session.user = tokenSet.claims(); // sub, role, email, ... req.session.tokens = { access_token: tokenSet.access_token, refresh_token: tokenSet.refresh_token, expires_at: tokenSet.expires_at }; res.redirect('/'); }); app.get('/me', (req, res) => { if (!req.session.user) return res.status(401).end(); res.json(req.session.user); });

7. curl walkthrough

Useful for poking at the IdP from a script or a Postman collection. Replace YOUR_CLIENT_ID, YOUR_REDIRECT_URI, and the PKCE values with your own.

Step 1 — open the authorize URL in a browser

https://phone.codeb.io/oidc.ashx?action=authorize\ &response_type=code\ &client_id=YOUR_CLIENT_ID\ &redirect_uri=https%3A%2F%2Fyour-app.com%2Foidc%2Fcallback\ &scope=openid+profile+email\ &state=abc123\ &code_challenge=YOUR_S256_CHALLENGE\ &code_challenge_method=S256

Sign in. The browser lands on your callback URL with ?code=<long string>&state=abc123.

Step 2 — exchange the code for tokens

curl -X POST https://phone.codeb.io/oidc.ashx?action=token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=<the code from step 1>" \ -d "redirect_uri=https://your-app.com/oidc/callback" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code_verifier=YOUR_PKCE_VERIFIER"

Response:

{ "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 3600, "id_token": "eyJhbGc...", "refresh_token": "7m...", "scope": "openid profile email" }

Step 3 — fetch the user profile

curl https://phone.codeb.io/oidc.ashx?action=userinfo \ -H "Authorization: Bearer eyJhbGc..."

8. Validate the ID token

If you accept ID tokens server-side (for example, to identify the user without a back-channel call), you MUST verify them. Skipping validation lets anyone hand you a forged token.

Any OIDC/JOSE library does this for you. The checks you need:

  1. Signature — verify against the public key from jwks_uri matching the kid in the JWT header.
  2. iss — must equal the discovery doc’s issuer (https://phone.codeb.io).
  3. aud — must equal your client_id.
  4. exp — must be in the future (allow a few seconds of clock skew).
  5. iat — sanity check: not far in the future.
  6. nonce — if you sent one in /authorize, it must echo back in the token.

Decoded ID token payload:

{ "iss": "https://phone.codeb.io", "sub": "alice", "aud": "YOUR_CLIENT_ID", "exp": 1748467200, "iat": 1748463600, "auth_time": 1748463600, "name": "Alice Walker", "preferred_username": "alice", "email": "alice@example.com", "email_verified": true, "role": "admin", "groups": ["admin"] }

The role claim is the CodeB-specific role assignment (admin, user, siponly, or guest). The standard groups claim defaults to a single-element array containing the role, which is what most RPs expect. Admins can override per user via the Groups field in the user profile editor (register.html) — when set, those groups replace the role-as-group default, which is the recommended way to feed RP-specific membership (Nextcloud groups, app roles, departments) into the token.

9. Refresh + logout

Refresh the access token

Access tokens last 1 hour. Refresh tokens last 4 hours and are rotated on every use (per OAuth 2.1 best practice): each call to /token with grant_type=refresh_token returns a fresh refresh_token, and the old one is invalidated immediately. Store the new one, discard the old.

curl -X POST https://phone.codeb.io/oidc.ashx?action=token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=YOUR_REFRESH_TOKEN" \ -d "client_id=YOUR_CLIENT_ID"

If you ever present an already-redeemed refresh token, the IdP returns invalid_grant. That’s the rotation security feature: it tells you the token has been used elsewhere (possible exfiltration). Force the user to re-sign-in.

Sign the user out

Clear your own session, then optionally redirect to the IdP’s end-session endpoint:

https://phone.codeb.io/oidc.ashx?action=end_session\ &post_logout_redirect_uri=https%3A%2F%2Fyour-app.com%2Floggedout

Because CodeB is cookie-free, the IdP-side logout is mostly a redirect — there’s no IdP session cookie to clear. The real logout work happens on your side: drop the tokens.

10. Token revocation (RFC 7009)

Need a user’s session to end immediately on the IdP side (admin action, password change, employee offboarding)? POST the refresh token to /oidc.ashx?action=revoke. The next time the RP tries to refresh, it gets invalid_grant and the user is forced to re-authenticate.

curl -X POST https://phone.codeb.io/oidc.ashx?action=revoke \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=<the_refresh_token>" \ -d "token_type_hint=refresh_token" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET"

Returns 200 {} regardless of whether the token existed — that’s RFC 7009 §2.2 deliberately preventing valid-token enumeration. Confidential clients must include client_secret; public clients may revoke without auth.

Access tokens are stateless JWTs and have a 1-hour TTL, so RFC 7009 §2.2 makes server-side revocation optional — CodeB no-ops on access-token revocation (still returns 200). To force-end a session faster than that, revoke the refresh_token AND have the RP drop its in-memory access_token.

11. Authentication factor (amr / acr)

Every issued id_token and access_token carries two claims that describe how the user actually authenticated:

RPs that need step-up auth (e.g. require a smart-card login for a sensitive operation) can branch on acr:

if (idToken.acr !== 'urn:codeb:acr:hwk-mfa') { // Force re-auth with stronger factor: // /authorize?acr_values=urn:codeb:acr:hwk-mfa&... redirectToAuthorizeWithAcrValues(); }

The claims survive refresh-token rotation, so the factor stays accurate for the life of the session. They’re also carried through the cookieless SSO assertion, so when the user signs into a second RP within the 30-min window the new RP sees the same factor.

12. Key rotation runbook (operator)

The IdP signs every JWT with a per-tenant 2048-bit RSA key. To rotate without downtime, CodeB supports an overlap window: two keys can be active at once. Tokens signed before rotation keep verifying via the previous key for as long as you keep it on disk; new tokens get signed with the freshly-generated key.

  1. SSH / RDP into the IIS host.
  2. Move the current key aside:
    cd D:\aloaha\phone\App_Data\phone.codeb.io\oidc move private-key.xml private-key-previous.xml
  3. The next request to /oidc.ashx on this tenant detects the missing private-key.xml and generates a fresh one. The previous key stays loaded as the verify-only fallback.
  4. JWKS now publishes both public keys (the new one first):
    curl https://phone.codeb.io/.well-known/jwks.json | jq .keys

    RPs that fetch JWKS regularly automatically pick up both. Tokens signed before the rotation continue to verify; new tokens use the new kid.

  5. Wait for the overlap to drain. Once every refresh token issued under the old key has been rotated (max 4 h with the default TTL), it’s safe to delete the previous key:
    del D:\aloaha\phone\App_Data\phone.codeb.io\oidc\private-key-previous.xml

    JWKS drops back to a single key on the next request; any straggler with a token signed by the old key will fail verification and have to re-authenticate.

The IdP mtime-watches both files so the rotation is hot — no IIS recycle, no dropped WebSocket sessions, no broken in-progress sign-ins. The kid in each JWT header pins it to the right key at verify time.

13. Troubleshooting

SymptomLikely cause
invalid_client at /authorize Your client_id isn’t registered for this tenant, or your redirect_uri doesn’t match the registered value byte-for-byte (case, trailing slash, query string).
invalid_request redirect Missing code_challenge or code_challenge_method != S256. PKCE is mandatory.
invalid_grant: PKCE verifier invalid The code_verifier you sent doesn’t SHA-256 to the code_challenge you sent earlier. Usually a typo or accidentally sending the challenge instead of the verifier.
invalid_grant: code invalid or expired Auth codes are single-use and live for 60 seconds. Don’t cache them; exchange immediately on callback.
invalid_grant: refresh_token invalid Refresh tokens are single-use (rotation). You presented one that was already redeemed — either by you on a previous call, or by an attacker if it leaked. Force re-login.
401 invalid_token on /userinfo Access token expired (1 hour), or the wrong tenant’s key signed it. Refresh and retry.
429 rate_limited on /login Per-IP rate limit (10 failed attempts per minute). Honour the Retry-After header.
State mismatch on callback Your front-end lost the state it stashed (e.g. user opened the auth URL in a different tab). Restart the flow.

14. Quick reference

WhatValue
Discovery/.well-known/openid-configuration
JWKS/.well-known/jwks.json
Authorize/oidc.ashx?action=authorize
Token/oidc.ashx?action=token
UserInfo/oidc.ashx?action=userinfo
End session/oidc.ashx?action=end_session
Signing algRS256 (2048-bit RSA, per-tenant key)
PKCErequired for all flows, S256 only
Grant typesauthorization_code, refresh_token
Access token TTL3600s (1 hour)
ID token TTL3600s (1 hour)
Refresh token TTL14400s (4 hours), rotated on every use
Auth code TTL60s, single-use
Scopesopenid, profile, email, groups, phone, address
Revocation endpoint/oidc.ashx?action=revoke
amr valuespwd, hwk, mfa, swk, otp
acr valuesurn:codeb:acr:pwd, urn:codeb:acr:hwk, urn:codeb:acr:hwk-mfa, urn:codeb:acr:mfa
Key rotationoverlap window: rename private-key.xml → private-key-previous.xml; new key auto-generated; both published in JWKS
Roles (in role claim)admin, user, siponly, guest

Need help wiring this up? Email info@codeb.io · Back to OIDC overview →

See also · platform integration guides