Appearance
OIDC endpoints
Reference for the OpenID Connect and OAuth 2 endpoints your organization serves, the tokens they issue, and the rate limits that apply. Most libraries only need the issuer and read the rest from discovery.
The examples use https://acme.fobauth.com. The console lists your organization's URLs under Endpoints on Settings > General. If you serve sign-in on a custom domain, every endpoint works there too, and the issuer matches the URL the app uses. See Custom domains.
For SAML endpoints, see SAML apps. For the management API and SCIM, see Management API and SCIM.
Endpoints
| Endpoint | Methods | Path |
|---|---|---|
| Discovery | GET | /.well-known/openid-configuration |
| Authorization server metadata (RFC 8414) | GET | /.well-known/oauth-authorization-server |
| Authorization | GET, POST | /oauth/authorize |
| Token | POST | /oauth/token |
| Device authorization (RFC 8628) | POST | /oauth/device_authorization |
| Userinfo | GET, POST | /oauth/userinfo |
| JWKS | GET | /oauth/jwks |
| Introspection (RFC 7662) | POST | /oauth/introspect |
| Revocation (RFC 7009) | POST | /oauth/revoke |
| Sign-out | GET, POST | /oauth/end-session |
Browser apps can call discovery, JWKS, token, device authorization, userinfo, introspection and revocation directly: they allow cross-origin requests from any origin and don't use cookies.
Discovery document values
Both discovery paths return the same document, with the URL of each endpoint above and these values:
| Field | Value |
|---|---|
response_types_supported | code |
response_modes_supported | query |
grant_types_supported | authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:device_code |
code_challenge_methods_supported | S256 |
token_endpoint_auth_methods_supported, revocation_endpoint_auth_methods_supported | client_secret_basic, client_secret_post, none |
introspection_endpoint_auth_methods_supported | client_secret_basic, client_secret_post |
id_token_signing_alg_values_supported | RS256 |
subject_types_supported | public |
scopes_supported | openid, profile, email, groups, offline_access |
prompt_values_supported | none, login |
claims_supported | sub, iss, aud, exp, iat, auth_time, nonce, azp, at_hash, sid, amr, act, email, email_verified, name, given_name, family_name, picture, updated_at, groups |
authorization_response_iss_parameter_supported | true |
request_parameter_supported, request_uri_parameter_supported, claims_parameter_supported | false |
Tokens
| Token | Format | Lifetime |
|---|---|---|
| Authorization code | Opaque, single use | 2 minutes |
| Access token | JWT (RFC 9068), typ is at+jwt | 15 minutes |
| ID token | JWT | 1 hour |
| Refresh token | Opaque, single use | 30 days from the original sign-in |
Tokens are signed with RS256 by a key in your organization's JWKS. An access token's aud is the app's own client ID. Fob doesn't support the resource or audience parameters (RFC 8707) and ignores them.
Refresh tokens
Ask for the offline_access scope to get a refresh token. Each refresh returns a new refresh token, and the old one stops working.
Refresh from one place at a time
Using a refresh token twice revokes every refresh token from that sign-in, so the user has to sign in again. This includes two requests sending the same token at the same moment.
Refresh tokens from one sign-in stop working 30 days after it, however often you refresh. They're also revoked when the user signs out, their session is ended, or they're disabled.
Fob never issues refresh tokens during an impersonation.
Scopes and claims
Apps get claims based on the scopes they ask for, in the ID token and from userinfo. Fob ignores scopes it doesn't recognise.
| Scope | Claims |
|---|---|
openid | sub, the user's ID, which never changes. Required. |
email | email and email_verified. The app's Email address sent setting picks which address. |
profile | name, given_name, family_name, picture, updated_at, and custom attributes with Send to apps on, by key |
groups | groups: the names of the user's groups. The built-in Everyone group isn't included. |
offline_access | A refresh token |
json
{
"iss": "https://acme.fobauth.com",
"sub": "0193a4f2-7c1e-7b3a-9f0e-2d4c5b6a7e8f",
"email": "ada@acme.com",
"name": "Ada Lovelace",
"employee_id": "E-1024",
"groups": ["Platform"]
}Group changes show up in the next token Fob issues, including tokens from a refresh.
To check a user's groups in a Node app with jose:
js
import { createRemoteJWKSet, jwtVerify } from "jose";
const issuer = "https://acme.fobauth.com";
const jwks = createRemoteJWKSet(new URL(`${issuer}/oauth/jwks`));
export async function requireGroup(idToken, group) {
const { payload } = await jwtVerify(idToken, jwks, { issuer, audience: "fob_8c1d2e3f4a5b6c7d8e9f0a1b" });
if (!payload.groups?.includes(group)) throw new Error(`Not in ${group}`);
return payload;
}How the user signed in: amr
ID tokens carry amr (RFC 8176). To require MFA in your app, check that it contains "mfa".
| Sign-in | amr |
|---|---|
| Password | ["pwd"] |
| Emailed reset or confirmation link | ["email"] |
| Passkey alone | ["hwk", "user", "mfa"] |
| Password and authenticator app | ["pwd", "otp", "mfa"] |
| Password and passkey | ["pwd", "hwk", "mfa"] |
| Password and recovery code | ["pwd", "kba", "mfa"] |
| Enterprise SSO | The identity provider's amr, if it sends one and Fob didn't ask for its own second factor |
ID tokens also carry sid, the user's Fob session. Access tokens carry sid while that session lasts.
Impersonation: act
When an admin impersonates a user, the ID token and access token carry act (RFC 8693), naming the admin. The ID token has no amr, because the user didn't sign in.
json
{
"sub": "0193a4f2-7c1e-7b3a-9f0e-2d4c5b6a7e8f",
"act": {
"sub": "0192ff10-3b4d-7e21-8c90-5a6b7c8d9e0f",
"email": "grace@acme.com"
}
}Check for act to block actions a helper shouldn't take on someone's behalf, such as approving expenses:
js
if (payload.act) {
return res.status(403).json({ error: "Not allowed while impersonating." });
}Userinfo and introspection responses don't include act.
Authorization
/oauth/authorize starts a sign-in with the authorization code flow. Send parameters in the query string, or as a form with POST.
| Parameter | Notes |
|---|---|
client_id | Required. |
redirect_uri | Required. Must exactly match one of the app's redirect URLs. For desktop and mobile apps, http://127.0.0.1, http://[::1] and http://localhost URLs match on any port. |
response_type | Required. Must be code. |
scope | Space-separated. Up to 1,024 bytes. |
state | Returned unchanged. Up to 2,048 bytes. |
nonce | Copied into the ID token. Up to 1,024 bytes. |
code_challenge, code_challenge_method | PKCE. The method must be S256. Required unless Require PKCE is off for a web app. |
response_mode | Only query. |
prompt | none returns an error instead of showing any page. login makes the user sign in again. none can't be combined with other values. |
max_age | Seconds. A session older than this signs in again. |
On success, Fob redirects to redirect_uri with code, state and iss. The code works once, within 2 minutes, and only while the user stays signed in. An unfinished sign-in expires after 30 minutes.
If the client ID is unknown, the redirect URL isn't registered, or a parameter is sent twice, Fob shows its own error page instead of redirecting. Other errors redirect with error, error_description, state and iss:
error | When |
|---|---|
invalid_request | Missing or bad PKCE, an unsupported response_mode, a non-numeric max_age, a parameter that's too long, or prompt=none combined with other values |
unsupported_response_type | response_type isn't code |
request_not_supported, request_uri_not_supported | The request used a request object |
login_required | prompt=none and the user isn't signed in |
access_denied | prompt=none and the user isn't assigned, or a sign-on policy denies them |
interaction_required | prompt=none and the sign-on policy needs the user to sign in again |
Token
/oauth/token takes a form-encoded POST.
Web apps authenticate with their client secret, using either HTTP Basic (client_secret_basic) or client_id and client_secret form fields (client_secret_post). Sending it both ways is refused. Browser, desktop and mobile apps send only client_id, and are refused if they send a secret.
grant_type | Parameters |
|---|---|
authorization_code | code, redirect_uri (the same one the sign-in used), and code_verifier if the sign-in sent a challenge |
refresh_token | refresh_token |
urn:ietf:params:oauth:grant-type:device_code | device_code. See Device authorization. |
The response has access_token, token_type (Bearer), expires_in, scope, id_token when the scope includes openid, and refresh_token when it includes offline_access. scope can be narrower than what was asked for.
A code is used up by the first attempt, even a failed one. Presenting a code again revokes the refresh tokens it produced.
error | Status | When |
|---|---|---|
invalid_client | 401 | Unknown client ID, wrong secret, a secret sent by a public app, or a SAML app's ID |
invalid_request | 400 | A missing parameter, a parameter or secret sent twice, or a body that isn't application/x-www-form-urlencoded |
invalid_grant | 400 | The code or refresh token is wrong, expired, used or revoked; redirect_uri or code_verifier doesn't match; the session has ended; or the user is disabled, can no longer use the app, or has no address at the app's email domain |
unauthorized_client | 400 | Device sign-in is off for the app |
authorization_pending, slow_down, access_denied, expired_token | 400 | Device sign-in, while waiting for the user. See below. |
unsupported_grant_type | 400 | Any other grant type |
temporarily_unavailable | 429 | Too many requests. See Rate limits. |
Device authorization
For command-line tools and devices without a good browser. The app shows a short code, the user enters it on another device, and the app polls for tokens. Turn on Allow the device authorization grant under Device sign-in on the app's General tab first.
- POST
client_idandscopeto/oauth/device_authorization. Web apps also send their secret. The response hasdevice_code,user_code(likeBCDF-GHJK),verification_uri(your organization's/devicepage),verification_uri_complete(the same with the code filled in),expires_in(600 seconds) andinterval(5 seconds). - Show the user
user_codeandverification_uri, or a QR code ofverification_uri_complete. - Every
intervalseconds, POSTgrant_type=urn:ietf:params:oauth:grant-type:device_code,device_codeandclient_idto/oauth/token, until it returns tokens.
error while polling | What to do |
|---|---|
authorization_pending | Keep polling. |
slow_down | Add 5 seconds to your polling interval. |
access_denied | The user declined, or signed out first. Stop. |
expired_token | The code expired. Start again. |
The user always signs in again after entering the code, even if they're already signed in. The app's assignments and sign-on policy apply. Each device code works once, and the tokens are the same as from a normal sign-in.
Userinfo
Call /oauth/userinfo with an access token in the Authorization: Bearer header. The response has sub and the claims the token's scopes allow.
| Status | error | When |
|---|---|---|
| 401 | invalid_token | The token is missing, expired or unknown, its session has ended, or the user can no longer use the app |
| 403 | insufficient_scope | The token doesn't include openid |
Introspection
/oauth/introspect tells a backend whether a token is still valid. Only web apps can call it, authenticating with their client secret. Send the token as the token form field.
An active access token returns active: true, token_type: "access_token", sub, client_id, scope, exp, iat, iss, aud and jti. An active refresh token returns active: true, token_type: "refresh_token", sub, client_id, scope, iat, exp and iss.
Everything else returns {"active": false}, including tokens issued to other apps and tokens for a user who has since been disabled, signed out or lost access to the app.
Revocation
/oauth/revoke revokes a refresh token and every other refresh token from the same sign-in. Send it as the token form field, authenticating as at the token endpoint. The response is 200 whether or not Fob knew the token.
Access tokens can't be revoked. They expire after 15 minutes, and introspection reports them inactive as soon as the user loses access.
Sign-out
/oauth/end-session signs the user out of Fob (RP-initiated logout). Send parameters in the query string, or as a form with POST.
| Parameter | Notes |
|---|---|
id_token_hint | An ID token Fob issued. Expired tokens are accepted. |
client_id | Identifies the app. Must match the ID token's audience if both are sent. |
post_logout_redirect_uri | Must be one of the app's Sign-out addresses. Needs client_id or id_token_hint. |
state | Added to the return URL. |
Fob signs the user out straight away only if id_token_hint names them and their current session. Otherwise it asks them to confirm. Without post_logout_redirect_uri, users land on the sign-in page.
If the ID token can't be verified, client_id doesn't match it, or the return URL isn't registered, Fob shows its own error page.
Rate limits
Over a limit, Fob responds 429 with a Retry-After header giving the seconds to wait. OAuth endpoints return "error": "temporarily_unavailable", the management API returns rate_limited, and SCIM uses its own error format.
Per IP address
Counted per minute, per IP address. IPv6 addresses are counted per /64. Each row has its own budget.
| Requests | Limit per minute |
|---|---|
Management API (/v1) and SCIM (/scim/v2), together | 3,000 |
| Token, introspection, revocation and device authorization, together | 600 |
Access API (/access/v1), used by fob and servers | 600 |
Authorization (/oauth/authorize) | 300 |
Enterprise SSO and SAML (/sso/…, /saml/…) | 120 |
| Opening a terminal in the browser | 60 |
| Signing in with a password or passkey | 30 |
| Entering a second-factor code | 30 |
| Opening an invitation, reset or confirmation link | 30 |
| Confirming it's you, and account changes that need it | 30 |
| Setting a new password when asked at sign-in | 30 |
| Looking up a user's identity provider from their email | 30 |
| Asking for a reset or confirmation email | 10 |
Entering a device code on /device | 10 |
Per app
Each app can make 3,000 requests a minute to the token, introspection, revocation and device authorization endpoints, from all addresses together.
Per account
These follow the user's account wherever the requests come from, counted over 15 minutes. They slow down guessing on top of lockout.
| Requests | Limit per 15 minutes |
|---|---|
| Password sign-in attempts | 20 |
| Second-factor and recovery codes | 10 |
| Tries at confirming it's you | 10 |
| Reset emails | 3 |
| Confirmation emails | 3 |
Emails and organizations
| What | Limit |
|---|---|
| Invitation, reset and email-change emails to one address, from the console and the API together | 5 an hour |
| The same emails from one organization in total | 500 a day |
| Admin invitations from one organization | 5 an hour to one address, 500 a day in total |
| Organizations one admin can create | 10 a day |
Users you create with a password through the management API don't count towards the email limits.