Skip to Content
API referenceOAuth and OpenID Connect

OAuth and OpenID Connect API

All protocol URLs belong to one tenant issuer. Discover them from the issuer instead of assembling paths in application code.

ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  "$ISSUER/.well-known/openid-configuration"

Response — 200 OK

{ "issuer": "https://auth.example.com", "authorization_endpoint": "https://auth.example.com/oauth2/authorize", "token_endpoint": "https://auth.example.com/oauth2/token", "jwks_uri": "https://auth.example.com/oauth2/jwks", "revocation_endpoint": "https://auth.example.com/oauth2/revoke", "introspection_endpoint": "https://auth.example.com/oauth2/introspect", "userinfo_endpoint": "https://auth.example.com/oauth2/userinfo", "response_types_supported": ["code"], "response_modes_supported": ["query"], "grant_types_supported": ["authorization_code", "refresh_token"], "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"], "revocation_endpoint_auth_methods_supported": ["client_secret_basic", "none"], "code_challenge_methods_supported": ["S256"], "scopes_supported": ["openid", "email", "offline_access"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], "request_uri_parameter_supported": false, "prompt_values_supported": ["none", "login", "consent", "select_account"] }

OAuth Authorization Server Metadata is also available according to RFC 8414. For a host-only issuer, request /.well-known/oauth-authorization-server. When the issuer includes a path, place that path after the well-known prefix. An unknown or disabled issuer returns 404 with an empty body.

Authorization Code with PKCE

Redirect the user agent to GET /oauth2/authorize.

ParameterRequirement
client_idregistered client identifier
redirect_uriexact registered redirect URI
response_typecode
scoperegistered scopes, normally including openid
stateunpredictable value bound to the initiating session
code_challengeSHA-256 PKCE challenge
code_challenge_methodS256
noncebind an ID token to the initiating client session
resourcetarget resource URI when requesting an API access token

After authentication and consent, Identity redirects to the registered callback:

HTTP/1.1 302 Found Location: com.example.app:/oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj&iss=https%3A%2F%2Fauth.example.com

Validate state before exchanging code. Validate iss against the issuer used to start the flow.

When the callback URI and client can be trusted, an authorization failure is returned to that callback:

HTTP/1.1 302 Found Location: https://app.example.com/callback?error=invalid_scope&state=af0ifjsldkj&iss=https%3A%2F%2Fauth.example.com
ErrorMeaning
invalid_requesta required parameter is absent, duplicated or malformed
unsupported_response_typeresponse_type is not code
invalid_scopeat least one requested scope is not registered for the client
invalid_targetthe requested resource is absent or unavailable to the client

An unknown client, an unregistered redirect_uri or an invalid issuer is never redirected. Identity returns 400 or 404 directly so attacker-controlled URLs cannot become callback targets.

Exchange an authorization code

Send POST /oauth2/token as application/x-www-form-urlencoded. Public clients send client_id; confidential clients use their registered authentication method.

ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=my-public-client' \
  --data-urlencode 'redirect_uri=com.example.app:/oauth/callback' \
  --data-urlencode 'code=AUTHORIZATION_CODE' \
  --data-urlencode 'code_verifier=PKCE_CODE_VERIFIER' \
  "$ISSUER/oauth2/token"

Response — 200 OK

{ "access_token": "eyJraWQiOiJrZXktMjAyNi0xMCIsImFsZyI6IlJTMjU2In0...", "token_type": "Bearer", "expires_in": 899, "refresh_token": "rt_7nDEmPwn4xT90O...", "scope": "email offline_access openid", "id_token": "eyJraWQiOiJrZXktMjAyNi0xMCIsImFsZyI6IlJTMjU2In0..." }
FieldMeaning
access_tokenbearer credential for the requested resource
token_typealways Bearer
expires_inaccess-token lifetime in seconds from the response time
refresh_tokenpresent when offline access was granted
scopespace-delimited scopes actually granted
id_tokenpresent for an OpenID Connect authorization

A refresh request uses grant_type=refresh_token, refresh_token, client authentication and an optional scope. The optional scope may only reduce the original grant.

Token endpoint errors

StatusErrorMeaning and client action
400invalid_requestfix missing, duplicate or malformed form fields, or use the required form content type
400unsupported_grant_typeuse authorization_code or refresh_token
400invalid_grantrestart authorization; the code or refresh token is invalid, expired, reused or does not match its client, redirect URI or PKCE verifier
400invalid_scoperequest only scopes present in the original refresh grant
401invalid_clientreplace the client authentication or use credentials registered for this issuer
404empty bodythe issuer does not identify an active tenant

UserInfo

GET or POST /oauth2/userinfo accepts a bearer access token.

Response — 200 OK

{ "sub": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M", "email": "ada@example.com", "email_verified": true }

sub is the tenant-local subject. email and email_verified are present only when the token authorizes the email scope.

An absent, invalid, expired or revoked bearer token returns:

HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token" Content-Type: application/json {"error":"invalid_token"}

Token introspection

Resource servers submit an access token to POST /oauth2/introspect. The request is form encoded and the resource server authenticates with HTTP Basic credentials issued for that resource.

ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  --user 'resource-server-id:RESOURCE_SERVER_SECRET' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'token=ACCESS_TOKEN' \
  "$ISSUER/oauth2/introspect"

Active token — 200 OK

{ "active": true, "scope": "catalog.read email", "client_id": "storefront-web", "token_type": "Bearer", "sub": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M", "aud": "lookandtry-api", "iss": "https://auth.example.com", "iat": 1791878400, "exp": 1791879300, "jti": "53729027-3696-48c7-a8b8-a461f75f7e46", "email": "ada@example.com", "email_verified": true }

Inactive or unknown token — 200 OK

{ "active": false }

Optional claims are omitted when unavailable or unauthorized. The email claims require the token’s email scope.

Security boundary

An HTTP 200 from introspection does not make a token usable. Require active: true, then enforce the expected iss, aud, scopes and product permissions.

Introspection returns 400 invalid_request for an invalid form and 401 invalid_client when the resource-server credential is rejected. The latter includes WWW-Authenticate: Basic realm="oauth2/introspect".

Signing keys

Request GET /oauth2/jwks through the discovered jwks_uri.

Response — 200 OK

{ "keys": [ { "kty": "RSA", "kid": "key-2026-10", "use": "sig", "alg": "RS256", "n": "uG5gkS4P...", "e": "AQAB" } ] }

Select the key whose kid matches the JWT header and verify alg before checking the signature. Cache the set according to response headers and refresh it when an unknown kid appears. Key rotation can expose more than one key at a time.

Revocation

Revoke one token

POST /oauth2/revoke accepts token as form data and authenticates the registered OAuth client. A valid request returns 200 OK with an empty body, including when the token is already inactive.

Errors are 400 invalid_request for an invalid form and 401 invalid_client for rejected client authentication.

Revoke a subject’s application grants

POST /oauth2/revoke-application-grants is for a resource server. Authenticate with its HTTP Basic credential and send exactly subject and client_id as form fields.

A successful request returns 204 No Content. Errors are 400 invalid_request, 401 invalid_client, or an empty 404 for an unknown issuer.