Skip to main content

OAuth & OIDC

O2ID implements the authorization-code grant with PKCE and OpenID Connect ID token issuance.

MethodPathDescription
GET/oauth2/authorizeStart the authorization flow
POST/oauth2/tokenExchange a code for tokens
GET/oauth2/userinfoReturn claims for a token holder
POST/oauth2/introspectReport what an access token carries
GET/oauth2/jwksPublic keys for ID token verification
GET/.well-known/openid-configurationOIDC discovery document

See Authorization Code Grant for a full walkthrough, and the API Reference for the complete OpenAPI contract.

Authorize

GET /oauth2/authorize starts the authorization-code flow. It redirects an authenticated user's browser back to the application with an authorization code; if no session exists yet, it redirects to the login portal first and resumes the authorization request once the user authenticates.

ParameterRequiredDescription
response_typeYesMust be code
client_idYesA registered application's clientId
redirect_uriYesA callback URL registered on the application
code_challengeYesPKCE challenge derived from a verifier
code_challenge_methodYesS256 or plain
scopeNoSpace-separated scopes
stateNoOpaque value round-tripped to the redirect URI

See Authorization Code Grant for PKCE generation and a complete request-to-callback walkthrough.

Token

POST /oauth2/token is O2ID's single token endpoint. Every grant is requested against this same path, distinguished by grant_type:

grant_typePurposeReference
authorization_codeExchange an authorization code for tokensAuthorization Code Grant
refresh_tokenExchange a refresh token for a new token setRefresh Token Grant
client_credentialsMachine-to-machine token issuance for an application or an AI agentClient Credentials Grant
urn:ietf:params:oauth:grant-type:token-exchangeDownscope or delegate an existing tokenToken Exchange

UserInfo

GET /oauth2/userinfo returns OIDC claims for the user identified by an access token. Pass the token as a Bearer credential:

curl -s http://localhost:8080/oauth2/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"

Response:

{
"sub": "9f8e7d6c5b4a39281726354433221100",
"email": "alice@example.com",
"name": "Alice"
}
FieldDescription
subThe user's internal ID
emailUser's email address
nameDisplay name (omitted when not set)

An invalid or expired token returns 401 Unauthorized with a WWW-Authenticate: Bearer challenge.

Token Introspection

O2ID's access tokens are opaque random strings, not JWTs: there is no header or payload to decode, and the token functions only as a lookup key into server-side storage. Token introspection (POST /oauth2/introspect, defined by RFC 7662) is the mechanism for determining what a given token carries — its subject, scopes, expiry, and related fields.

Because introspection can reveal information about any token, not only one the caller itself holds, the endpoint is not open. There's no admin/operator path: the only caller who can ever introspect a token is an application's own client_credentials token (see Managing Resource Servers), and even then only for a token whose aud names a resource server that application is already authorized for. No separate scope grant is needed for this — the resource server authorization itself is the permission. Introspecting any other token, or one with no aud at all, returns the same {"active": false} as a token that doesn't exist, per RFC 7662 §2.2's opacity requirement below.

curl -s -X POST http://localhost:8080/oauth2/introspect \
-H "Authorization: Bearer ${RESOURCE_SERVER_APP_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "token=${ACCESS_TOKEN}"
{
"active": true,
"scope": "users:read",
"client_id": "client_abc123",
"sub": "9f8e7d6c5b4a39281726354433221100",
"token_type": "Bearer",
"exp": 1735689600,
"aud": "https://api.example.com"
}

aud is what makes this introspectable at all — a token minted with no resource/audience (see Token Exchange) can never be introspected by anyone, since there's no resource server standing to check it against.

If the introspected token was minted by actor-token delegation, the response also includes RFC 8693 §4.1's act claim. sub continues to identify the human; act.sub names the AI agent currently acting on their behalf:

{
"active": true,
"scope": "users:read",
"client_id": "client_abc123",
"sub": "9f8e7d6c5b4a39281726354433221100",
"token_type": "Bearer",
"exp": 1735689600,
"aud": "https://api.example.com",
"act": { "sub": "agent_e241a1c4447bdc04d755a707dd0dec76" }
}

Per RFC 7662 §2.2, an unknown, expired, or otherwise malformed token is never treated as an error: the response is always 200 OK with exactly {"active": false} and no additional fields. This prevents a caller from distinguishing "this token does not exist" from "this token expired" by response shape, and also makes introspection a suitable way to check whether a token remains valid before use. Refresh tokens are not supported by this endpoint; only access tokens can be introspected.

JWKS

GET /oauth2/jwks returns the public key used to verify ID token signatures, as a JSON Web Key Set (RFC 7517):

curl -s http://localhost:8080/oauth2/jwks
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "o2id-1",
"alg": "RS256",
"n": "…",
"e": "AQAB"
}
]
}

O2ID signs ID tokens with a single RSA key pair, generated in memory each time the server starts. The key is not persisted across restarts: an ID token issued before a restart cannot be verified against the JWKS returned after one. This endpoint is what a relying party fetches to verify an ID token's signature independently of the token endpoint.

OIDC Discovery

GET /.well-known/openid-configuration returns the OIDC discovery document, letting a relying party locate O2ID's other endpoints and determine which capabilities are supported, without hardcoding paths:

curl -s http://localhost:8080/.well-known/openid-configuration
{
"issuer": "http://localhost:8080",
"authorization_endpoint": "http://localhost:8080/oauth2/authorize",
"token_endpoint": "http://localhost:8080/oauth2/token",
"userinfo_endpoint": "http://localhost:8080/oauth2/userinfo",
"jwks_uri": "http://localhost:8080/oauth2/jwks",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "email", "profile"],
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "nonce", "email", "name"],
"code_challenge_methods_supported": ["S256", "plain"]
}
FieldDescription
issuerO2ID's issuer identifier, used as the iss claim in ID tokens
authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uriAbsolute URLs for the endpoints described above
response_types_supportedAlways ["code"] — O2ID supports only the authorization-code response type
subject_types_supportedAlways ["public"] — the same sub value is returned to every client, not pairwise-unique per client
id_token_signing_alg_values_supportedAlways ["RS256"]
scopes_supportedThe OIDC scopes recognized by the authorization endpoint
claims_supportedThe claims O2ID may include in an ID token or the UserInfo response
code_challenge_methods_supportedPKCE methods accepted at the authorization endpoint

/oauth2/introspect is deliberately absent from this document today: RFC 8414 defines introspection_endpoint as an optional field, and O2ID does not yet advertise it here.