Obexal Docs

Docs/Authentication/OIDC and OAuth 2.1

OIDC and OAuth 2.1 provider

How Obexal implements OpenID Connect and OAuth 2.1: discovery, JWKS, supported and unsupported flows, scopes, claims and client types.

Obexal is a standards-compliant OpenID Connect provider built on OAuth 2.1. Every application you register (web app, SPA, backend service or AI agent) is an OAuth client, and any standard OIDC library can integrate with it: there is no proprietary SDK to embed.

Discovery and JWKS

The discovery document is the single source of truth for endpoints and capabilities:

curl https://accounts.obexal.com/.well-known/openid-configuration
{
  "issuer": "https://accounts.obexal.com",
  "authorization_endpoint": "https://accounts.obexal.com/oauth/authorize",
  "pushed_authorization_request_endpoint": "https://accounts.obexal.com/oauth/par",
  "token_endpoint": "https://accounts.obexal.com/oauth/token",
  "userinfo_endpoint": "https://accounts.obexal.com/oauth/userinfo",
  "jwks_uri": "https://accounts.obexal.com/.well-known/jwks.json",
  "revocation_endpoint": "https://accounts.obexal.com/oauth/revoke",
  "introspection_endpoint": "https://accounts.obexal.com/oauth/introspect",
  "end_session_endpoint": "https://accounts.obexal.com/oauth/logout",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"],
  "code_challenge_methods_supported": ["S256"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "profile", "email"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post", "private_key_jwt"]
}

The JWKS at /.well-known/jwks.json contains public keys only. It publishes the active signing key plus retired keys that have not yet expired, so tokens issued before a key rotation keep validating.

Note

Examples use accounts.obexal.com, the default domain. If your organization uses a custom domain, that domain becomes the issuer: replace it everywhere.

Supported flows

  • Authorization code with PKCE: the only interactive flow. PKCE is mandatory for every client, public and confidential alike; code_challenge_method must be S256 (plain is refused, and so is a missing challenge).
  • Client credentials: machine identity for backend services and AI agents. Confidential clients only; the token carries sub equal to the client_id, with no ID token and no refresh token.
  • Refresh token: rotating, with replay detection. See Validate tokens.
  • Token exchange (RFC 8693): attributable delegation for AI agents. See Delegation with Token Exchange.

Deliberately not supported: the implicit flow (response_type other than code), the resource owner password grant, and the device authorization grant. Any other grant_type receives the standard error unsupported_grant_type. OAuth endpoints return protocol-standard errors of the form {"error": "...", "error_description": "..."}.

Client types

TypeToken endpoint authTypical use
Publicnone (no secret, PKCE only)SPA, mobile app, CLI
Confidentialclient_secret_basic, client_secret_post or private_key_jwtServer-side web app, service, AI agent

You register clients in the admin console or via POST /v1/applications. A confidential client's secret is returned once at creation; only its SHA-256 hash is stored. Redirect URIs must be absolute http(s) URLs and are matched exactly, with no wildcards.

Scopes and claims

The standard scopes are openid (required), profile and email.

The access token is a JWT (header typ: at+jwt, signed RS256) with claims iss, sub, aud (the client_id by default), client_id, exp, iat, jti, scope and tenant. Delegated tokens add an act claim (the acting agent), and DPoP-bound tokens add cnf.jkt.

The ID token (header typ: JWT) carries iss, sub, aud, exp, iat, auth_time and nonce (when supplied), plus:

  • groups: the names of the user's groups, omitted when the user belongs to none.
  • email and email_verified with the email scope.
  • given_name, family_name, name and locale with the profile scope, when set on the directory profile.

The authorization code flow, step by step

Generate a PKCE pair on the client:

VERIFIER=$(openssl rand -base64 60 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=\n')

Send the user's browser to the authorization endpoint. With a valid Obexal session, the response is a 302 back to your redirect_uri; without one, the user is first sent to the hosted sign-in page and then returns here.

curl -sS -i -b cookies.txt "https://accounts.obexal.com/oauth/authorize?response_type=code&client_id=app_a1b2c3&redirect_uri=https%3A%2F%2Fapp.example.eu%2Fcallback&scope=openid%20profile%20email&state=xyz&nonce=n-123&code_challenge=$CHALLENGE&code_challenge_method=S256"
# 302 Location: https://app.example.eu/callback?code=<CODE>&state=xyz

Exchange the code (single use; a replay returns invalid_grant):

curl -sS -X POST https://accounts.obexal.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=<CODE>' \
  -d 'redirect_uri=https://app.example.eu/callback' \
  -d 'client_id=app_a1b2c3' \
  -d "code_verifier=$VERIFIER"
{
  "access_token": "<JWT RS256>",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "<opaque>",
  "id_token": "<JWT>",
  "scope": "openid profile email"
}

A confidential client authenticates on top of PKCE, for example with -u "$CLIENT_ID:$CLIENT_SECRET" (HTTP Basic).

Machine identity: client_credentials

curl -sS -X POST https://accounts.obexal.com/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d 'grant_type=client_credentials' \
  -d 'scope=read'
# 200 -> {"access_token":"<JWT>","token_type":"Bearer","expires_in":600,"scope":"read"}

The requested scope must be a subset of the client's registered scopes.

Going further