Skip to main content
Version: Latest (v0.0.1)

Authorization Code Grant

The OAuth2 authorization-code grant is the primary flow O2ID supports. It issues a short-lived authorization code to the client after the user authenticates, which the client then exchanges for an access token and refresh token. PKCE is required.

Flow Overview

  1. Client starts the flow — redirect the user's browser to /oauth2/authorize with the required parameters.
  2. Login — if the user has no active session, O2ID redirects to the login portal. After the user authenticates, O2ID resumes the authorization request automatically.
  3. Consent — for third-party applications, O2ID redirects to the consent screen so the user can approve the requested scopes. Trusted applications and authorizations already covered by a remembered consent grant skip this step. See Consent below.
  4. Authorization code issued — O2ID redirects back to the registered redirect_uri with a code query parameter and the original state value.
  5. Token exchange — the client posts the code, PKCE verifier, and client_id to /oauth2/token and receives tokens.

PKCE

PKCE (Proof Key for Code Exchange) is required for all authorization requests. Both plain and S256 challenge methods are supported. Use S256 in production.

Generate a verifier and challenge:

CODE_VERIFIER="a-long-random-string-at-least-43-chars"

# S256 challenge
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| openssl base64 -A \
| tr '+/' '-_' \
| tr -d '=')

Authorization Request

GET /oauth2/authorize

Required query parameters:

ParameterValue
response_typecode
client_idThe clientId of a registered application
redirect_uriA callback URL registered on the application
code_challengePKCE challenge derived from the verifier
code_challenge_methodS256 or plain

Optional query parameters:

ParameterDescription
scopeSpace-separated scopes, e.g. openid profile
stateOpaque string round-tripped to the redirect URI

Success response: 302 Found. For a third-party application this redirects to the consent screen (/consent?consent_id=…); once consent is granted (or for a trusted application), it redirects to redirect_uri?code=…&state=….

Before an authorization code is issued, O2ID shows the user a consent screen listing the scopes the application is requesting. This step is skipped when:

  • The application is marked trusted (first-party). Set this with o2idctl applications create --trusted (or --untrusted to clear it on update). The seeded o2idctl CLI application is trusted.
  • The user has already consented to this application for scopes that cover the current request. O2ID remembers each decision, so repeat authorizations don't prompt again — unless the request adds a scope that wasn't previously granted, which prompts for the new scope.

Endpoint: POST /consent (form-encoded), submitted by the consent page.

FieldValue
consent_idThe consent transaction ID from the /consent redirect
actionapprove or deny

On approve, O2ID redirects to redirect_uri?code=…&state=…. On deny, it redirects to redirect_uri?error=access_denied&state=…. The authoritative pending authorization (subject and resolved scopes) is held server-side keyed by consent_id — the form body can't widen it.

The consent page is part of the login portal — it's served (and, if you host the portal externally, hosted) alongside the login page. See Login Portal.

Token Request

POST /oauth2/token

Content type: application/x-www-form-urlencoded

Required fields:

FieldValue
grant_typeauthorization_code
codeThe authorization code from the redirect
client_idSame clientId used in the authorization request
redirect_uriSame redirect URI used in the authorization request
code_verifierThe original PKCE verifier

Success response:

{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "openid profile"
}

Authorization codes are single-use. Reusing a code after a successful exchange returns invalid_grant.

Full cURL Walkthrough

Every endpoint is served under a tenant path prefix (/t/{tenant}/…); this walkthrough uses the system tenant that o2id serve seeds on first run. See Multi-Tenancy for the tenancy model.

This walkthrough runs the complete flow from scratch using curl, jq, and openssl. Start O2ID before running these commands:

o2id serve --addr :8080

Create an application. The callback URL must match the redirect_uri used during authorization:

APP_JSON=$(curl -s -X POST http://localhost:8080/t/system/applications \
-H "Content-Type: application/json" \
-d '{"name":"Demo App","callbackUrls":["http://localhost:3000/callback"]}')

CLIENT_ID=$(printf '%s' "$APP_JSON" | jq -r .clientId)

Create a user with a password:

curl -s -X POST http://localhost:8080/t/system/users \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","displayName":"Alice","password":"secret"}'

Log in and save the session cookie:

curl -i -c /tmp/o2id-cookies.txt -X POST http://localhost:8080/t/system/login \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "email=alice@example.com&password=secret"

Generate a PKCE verifier and S256 challenge:

CODE_VERIFIER="verifier-1"
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| openssl base64 -A \
| tr '+/' '-_' \
| tr -d '=')

Authorize the logged-in user:

curl -i -b /tmp/o2id-cookies.txt \
"http://localhost:8080/t/system/oauth2/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=openid%20profile&state=demo-state&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256"

Since the demo application is third-party, the response is a 302 Found redirect to the consent screen (/consent?consent_id=…). Copy the consent_id from the Location header and approve it:

CONSENT_ID="paste-consent_id-from-location-header"

curl -i -X POST http://localhost:8080/t/system/consent \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "consent_id=${CONSENT_ID}" \
--data-urlencode "action=approve"

(Creating the application with "trusted":true would skip this step and redirect straight to the callback.)

This response is a 302 Found redirect to the registered callback URL. Copy the code query parameter from the Location header.

Exchange the authorization code for tokens:

AUTH_CODE="paste-code-from-location-header"

curl -s -X POST http://localhost:8080/t/system/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=${AUTH_CODE}" \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "redirect_uri=http://localhost:3000/callback" \
--data-urlencode "code_verifier=${CODE_VERIFIER}"

Call the UserInfo endpoint with the access token:

ACCESS_TOKEN=$(printf '%s' "$TOKEN_JSON" | jq -r .access_token)

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