AAuthfy Docs

OAuth 2.0 / OIDC endpoints

External apps use the Authorization Code + PKCE flow for SSO. It is the only way to obtain application-bound JWTs.

MethodEndpointPurposeAuth
GET/.well-known/openid-configurationOIDC discovery document.Public
GET/api/v1/oauth/jwksAll usable access-key public keys as JWKS. ?kid=<fingerprint> narrows to all usable keys of that key's application.Public
GET/api/v1/oauth/authorizeValidate an authorization request for the UI consent page.Public
POST/api/v1/oauth/authorizeIssue an authorization code after the Auth UI session approves the request.Auth UI bearer token
GET/api/v1/oauth/logoutValidate a post-logout redirect URI against the access key's Allowed URLs.Public
POST/api/v1/oauth/tokenExchange authorization code + PKCE verifier, or rotate a refresh token.Public / server-to-server (X-App-Public-Key)
POST/api/v1/oauth/revokeRevoke a refresh token during external-app logout.Public / server-to-server (X-App-Public-Key)
GET/api/v1/oauth/userinfoReturn the OIDC user profile for a valid access token.Bearer access token

There is no OAuth client_id. The client identifies itself with its access key everywhere:

Authorization requests are sent to the UI browser endpoint configured as OAUTH_AUTHORIZATION_ENDPOINT (default http://localhost:3000/oauth/authorize):

GET {authViewUrl}/oauth/authorize?response_type=code&key_id=<key fingerprint>&redirect_uri=https%3A%2F%2Fhrm.example.com%2Fcallback&scope=openid%20profile%20email&state=<state>&nonce=<nonce>&code_challenge=<challenge>&code_challenge_method=S256

Logout redirects use the same identifier:

GET {authViewUrl}/oauth/logout?post_logout_redirect_uri=https%3A%2F%2Fhrm.example.com%2Fsigned-out&key_id=<key fingerprint>

The token endpoint is the API endpoint:

POST /api/v1/oauth/token
Content-Type: application/x-www-form-urlencoded
X-App-Public-Key: -----BEGIN PUBLIC KEY-----\nMIIB...\n-----END PUBLIC KEY-----

grant_type=authorization_code&redirect_uri=https%3A%2F%2Fhrm.example.com%2Fcallback&code=<code>&code_verifier=<verifier>

The presented key must belong to the same application as the authorization code or refresh token being exchanged — tokens survive key rotation but never cross applications.

Token response covering

The access_token, id_token, and refresh_token response fields are returned with a lightweight cvr1 AES-GCM cover derived from the access key public key supplied in X-App-Public-Key. External app backends must uncover those values with the same public key before JWT verification/use.

After uncovering, access and ID tokens are RS256 JWTs signed with the presented access key's private key. The JWT header kid equals the access key's public-key fingerprint and matches the key published by /api/v1/oauth/jwks?kid=<fingerprint>.

The redirect_uri and post_logout_redirect_uri are validated against the access key's Allowed URLs. The user's company is resolved server-side from the user's active memberships subscribed to the requested application — clients do not pick the company.

Allowed URLs

Every access key carries a list of Allowed URLs, set when the key is created on the support console's Access keys page. It is the only list the API will redirect a signed-in user to for requests made with that key.

Format — one URL per entry

The list is stored as text and split on whitespace or commas. Put each URL on its own line (or separate them with commas):

https://smartfarm-ui.caprover.truecode.africa
http://localhost:3005

Both forms parse to the same two entries:

https://smartfarm-ui.caprover.truecode.africa, http://localhost:3005

Each entry must be an absolute http:// or https:// URL with a host, and no query string and no #fragment. Trailing slashes are stripped on save.

Do not paste an escaped \n between URLs. If the backslash is lost in transit the list becomes a single glued entry such as https://smartfarm-ui.caprover.truecode.africa/nhttp://localhost:3005 — a syntactically valid URL, so it saves without error, but neither real origin is registered and every sign-in fails the return URL check. After creating the key, open it and confirm you see one URL per line.

How a redirect is matched

An incoming redirect_uri is allowed when some entry matches on both:

Allowed URLredirect_uriResult
http://localhost:3005http://localhost:3005/api/authy/callbackAllowed — entry path is /
http://localhost:3005/api/authyhttp://localhost:3005/api/authy/callbackAllowed — below the entry path
http://localhost:3005/api/authy/callbackhttp://localhost:3005/signed-outRejected — different path
http://localhost:3005http://localhost:3000/api/authy/callbackRejected — different port
https://smartfarm-ui.caprover.truecode.africahttp://smartfarm-ui.caprover.truecode.africa/cbRejected — different scheme

Registering the bare origin is usually what you want: it covers the callback, the post-logout URL, and any future route without needing a replacement key.

Local development

http:// and localhost are accepted — there is no special-casing that blocks them. To run a local build against a deployed Authfy, add your dev origin alongside the production one, matching the port your dev server actually uses:

https://smartfarm-ui.caprover.truecode.africa
http://localhost:3005

Be aware of what this means: anyone who can start an authorization request with that key_id can have the code delivered to a listener on their own machine. If that is not acceptable for a production application, create a separate development access key instead — with only the local origin in its Allowed URLs — and point the local .env at its public key.

Token creation rules

The authfy npm package wires this whole flow — including PKCE, the token cover, silent refresh, and logout — into a Next.js app with two files.