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.
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_methodmust beS256(plainis refused, and so is a missing challenge). - Client credentials: machine identity for backend services and AI agents. Confidential clients only; the token carries
subequal to theclient_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
| Type | Token endpoint auth | Typical use |
|---|---|---|
| Public | none (no secret, PKCE only) | SPA, mobile app, CLI |
| Confidential | client_secret_basic, client_secret_post or private_key_jwt | Server-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.emailandemail_verifiedwith theemailscope.given_name,family_name,nameandlocalewith theprofilescope, 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=xyzExchange 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
- Validate tokens in your API: JWKS versus introspection, revocation, refresh rotation.
- Advanced client security: PAR, DPoP and
private_key_jwt. - Sessions and logout: RP-initiated and Back-Channel Logout.
- Delegation with Token Exchange: AI agents acting on behalf of users.