barycenter/docs/flows.md
Claude 7b16f54223
feat: Add token introspection endpoint, docs, and validation scripts
Implement the remaining items from docs/next-iteration-plan.md:

- Add POST /introspect endpoint (RFC 7662) with client authentication,
  support for access and refresh tokens, and token_type_hint
- Add raw token lookup functions in storage for introspection
- Add revocation_endpoint and introspection_endpoint to discovery metadata
- Create docs/flows.md with end-to-end curl examples for all OIDC flows
- Create scripts/validate-oidc.sh to verify discovery, JWKS, registration,
  introspection, and revocation endpoints
- Update docs/oidc-conformance.md to reflect actual implementation status
- Update README.md and CLAUDE.md pending sections to be accurate

https://claude.ai/code/session_01JBxVy75XfwwZB8iBXjTxT3
2026-03-19 20:30:31 +00:00

4.7 KiB

End-to-End OIDC Flows

This document provides example curl/browser commands for the complete Authorization Code + PKCE flow.

All examples assume the server is running at http://localhost:9090.


1. Check Discovery

curl -s http://localhost:9090/.well-known/openid-configuration | jq .

Verify key fields: issuer, authorization_endpoint, token_endpoint, jwks_uri, userinfo_endpoint, introspection_endpoint, revocation_endpoint.


2. Register a Client

curl -s -X POST http://localhost:9090/connect/register \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["http://localhost:8080/callback"],
    "client_name": "Test Client",
    "token_endpoint_auth_method": "client_secret_basic"
  }' | jq .

Save client_id and client_secret from the response.


3. Generate PKCE Challenge

# Generate code_verifier (43-128 chars, base64url)
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')

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

echo "code_verifier:  $CODE_VERIFIER"
echo "code_challenge: $CODE_CHALLENGE"

4. Start Authorization (Browser)

Open this URL in a browser (replace CLIENT_ID with your actual client_id):

http://localhost:9090/authorize?client_id=CLIENT_ID&redirect_uri=http://localhost:8080/callback&response_type=code&scope=openid%20profile%20email&code_challenge=CODE_CHALLENGE&code_challenge_method=S256&state=random123&nonce=nonce456

If not logged in, you will be redirected to /login. Enter credentials (e.g., admin / password123).

After login and consent, the browser redirects to:

http://localhost:8080/callback?code=AUTH_CODE&state=random123

Copy the code parameter.


5. Exchange Code for Tokens

Using client_secret_basic (HTTP Basic auth):

# Base64 encode client_id:client_secret
AUTH=$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64 -w0)

curl -s -X POST http://localhost:9090/token \
  -H "Authorization: Basic $AUTH" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:8080/callback&code_verifier=$CODE_VERIFIER" | jq .

Using client_secret_post (form body):

curl -s -X POST http://localhost:9090/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:8080/callback&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&code_verifier=$CODE_VERIFIER" | jq .

The response includes access_token, id_token, token_type, and expires_in.


6. Decode the ID Token

# Extract and decode the JWT payload (second segment)
echo "$ID_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

Expected claims: iss, sub, aud, exp, iat, auth_time, nonce, at_hash, amr, acr.


7. Call UserInfo

curl -s http://localhost:9090/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

Returns claims based on the granted scopes (e.g., sub, name, email).

Error case (missing/invalid token):

curl -s -w "\nHTTP %{http_code}\n" http://localhost:9090/userinfo
# Returns 401 with WWW-Authenticate header

8. Introspect a Token

AUTH=$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64 -w0)

curl -s -X POST http://localhost:9090/introspect \
  -H "Authorization: Basic $AUTH" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$ACCESS_TOKEN" | jq .

Active token response:

{
  "active": true,
  "scope": "openid profile email",
  "client_id": "...",
  "sub": "...",
  "exp": 1234567890,
  "iat": 1234564290,
  "token_type": "bearer"
}

Expired/revoked/unknown token:

{
  "active": false
}

9. Revoke a Token

AUTH=$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64 -w0)

curl -s -X POST http://localhost:9090/revoke \
  -H "Authorization: Basic $AUTH" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$ACCESS_TOKEN"
# Returns 200 OK with empty body

After revocation, introspecting the same token returns {"active": false}.


10. Refresh Token Flow

If the offline_access scope was granted, a refresh_token is included in the token response.

AUTH=$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64 -w0)

curl -s -X POST http://localhost:9090/token \
  -H "Authorization: Basic $AUTH" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token&refresh_token=$REFRESH_TOKEN" | jq .

Token rotation: the old refresh token is revoked and a new one is issued.