OAuth and OIDC endpoints
Every endpoint of a tenant: discovery, authorize, token, userinfo, revoke, introspect, logout and device, with parameters and responses.
Every endpoint is on the tenant's issuer: https://<tenant>.9in.io, https://<tenant>-sandbox.9in.io or your custom domain. <issuer> below stands for it.
| Method | Path | What |
|---|---|---|
| GET | /.well-known/openid-configuration | Discovery document |
| GET | /.well-known/jwks.json | Public signing keys |
| GET, POST | /oauth/authorize | Start a sign-in |
| POST | /oauth/token | Exchange a code, refresh, client credentials, device code, password |
| POST | /oauth/device/authorize | Start a device sign-in (RFC 8628) |
| GET, POST | /oauth/userinfo | The user's claims, for an access token |
| POST | /oauth/revoke | Revoke a refresh or access token (RFC 7009) |
| POST | /oauth/introspect | Is a token live? (RFC 7662) |
| GET, POST | /oauth/logout | Sign out (RP-initiated logout) |
/api/v1/… | The Management API |
The hosted pages (/login, /register, /password/recovery, /account, /device, …) are for people, not for API calls; link to /account from your app if you like.
#Discovery
GET <issuer>/.well-known/openid-configuration returns the endpoints above and what's supported:
| Field | Value |
|---|---|
response_types_supported | code, id_token, id_token token |
response_modes_supported | query, fragment, form_post |
grant_types_supported | authorization_code, refresh_token, client_credentials, urn:ietf:params:oauth:grant-type:device_code, password, implicit (the last two only for applications that enable them) |
code_challenge_methods_supported | S256 |
token_endpoint_auth_methods_supported | client_secret_basic, client_secret_post, none (public clients) |
id_token_signing_alg_values_supported | The algorithms of the tenant's keys |
prompt_values_supported | none, login, consent, select_account, create |
ui_locales_supported | The tenant's languages |
authorization_response_iss_parameter_supported | true |
scopes_supported | The standard scopes, plus every API's scopes |
#GET /oauth/authorize
Redirect the browser here to sign a user in. POST with the same parameters as a form also works.
| Parameter | Required | |
|---|---|---|
response_type | Yes | code. (id_token and id_token token only for applications with the implicit flow turned on.) |
client_id | Yes | |
redirect_uri | Yes | Must exactly match one of the application's callback URLs |
scope | Space-separated. Include openid for an ID token, offline_access for a refresh token. Default openid. | |
state | Recommended | Returned unchanged |
code_challenge, code_challenge_method | Public clients | PKCE: S256 only |
nonce | Copied into the ID token | |
audience (or resource) | An API identifier: the access token is issued for that API | |
prompt | login (sign in again), none (no UI: fail with login_required or consent_required), consent, select_account, create (open the sign-up form) | |
max_age | Seconds: ask the user to sign in again if their last sign-in is older | |
login_hint | Pre-fills the identifier | |
ui_locales | Preferred languages, e.g. de en | |
response_mode | query (default for code), fragment, form_post |
request and request_uri (signed request objects) are not supported.
Success: a redirect to redirect_uri?code=…&state=…&iss=<issuer>. Check state, and iss if your library supports it.
Errors: with a valid client and redirect URI, a redirect to redirect_uri?error=…&error_description=…&state=…: invalid_request, unauthorized_client, unsupported_response_type, invalid_scope, invalid_target (unknown audience), access_denied (the user declined), login_required, consent_required, request_not_supported. An unknown client or redirect URI shows an error page instead of redirecting.
#POST /oauth/token
Form-encoded (application/x-www-form-urlencoded). Confidential applications authenticate with HTTP Basic (client_id:client_secret, URL-encoded) or client_id and client_secret in the form; public applications send client_id. Browsers may call it from an allowed web origin (CORS).
grant_type | Parameters |
|---|---|
authorization_code | code, redirect_uri, code_verifier (PKCE) |
refresh_token | refresh_token, optional narrower scope |
client_credentials | Optional audience and scope (machine to machine) |
urn:ietf:params:oauth:grant-type:device_code | device_code (devices) |
password | username, password, scope, optional audience (first-party applications that enable it; refused for users with two-factor) |
{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email offline_access",
"id_token": "eyJ…",
"refresh_token": "…"
}id_token comes with the openid scope; refresh_token with offline_access (when the application and the API allow refresh tokens). Refresh tokens rotate: each refresh returns a new one, and using an old one again revokes the whole family.
Errors (JSON { "error", "error_description" }): invalid_request, invalid_client (401), invalid_grant (bad or used code, wrong verifier, expired or revoked refresh token, wrong password), unauthorized_client (grant not allowed for this application), unsupported_grant_type, invalid_scope, invalid_target, access_denied, and for device codes authorization_pending, slow_down, expired_token.
#POST /oauth/device/authorize
client_id, scope, optional audience. Returns device_code, user_code (XXXX-XXXX), verification_uri (<issuer>/device), verification_uri_complete, expires_in (600) and interval (5). See TVs, CLIs and devices.
#GET /oauth/userinfo
Authorization: Bearer <access token> (a token with the openid scope). Returns sub plus the claims the token's scopes allow (scopes and claims) and custom fields marked as claims. Revoked tokens get 401.
#POST /oauth/revoke
token, optional token_type_hint (refresh_token or access_token), with client authentication (or client_id for public clients). Revoking a refresh token revokes its family; revoking a JWT access token puts its jti on a deny-list checked by introspection and hybrid verification. Always answers 200 for an unknown token, as RFC 7009 requires.
#POST /oauth/introspect
token, authenticated as a confidential application of the tenant. Live state: revoked tokens, ended sessions and blocked users are { "active": false }.
{
"active": true,
"token_type": "access_token",
"scope": "orders:read",
"client_id": "app_7f3k2m9q",
"sub": "usr_8k2…",
"aud": "https://api.acme.com",
"iss": "https://acme.9in.io",
"exp": 1794203600,
"iat": 1794200000,
"jti": "…",
"sid": "…",
"auth_time": 1794199000
}#GET /oauth/logout
Ends the 9in session, then redirects to post_logout_redirect_uri (with state) or shows a "You're signed out" page.
| Parameter | |
|---|---|
id_token_hint | An ID token the tenant issued (expired is fine): identifies the application |
client_id | Identifies the application when there's no id_token_hint |
post_logout_redirect_uri | One of the application's allowed logout URLs |
state | Added to the redirect |
#GET /.well-known/jwks.json
The public keys that verify tokens: every active and retired asymmetric key (RS, PS, ES, EdDSA). HMAC keys are never published. Cache it; fetch it again when a token names a kid you don't know.