Hanzo
PlatformHanzo IAMConnecting to IAM

OIDC Flows

Step-by-step guide to OpenID Connect authorization flows with Hanzo IAM — authorization code, PKCE, client credentials, device code, and token refresh.

OIDC Flows

Hanzo IAM implements the full OpenID Connect specification. This page walks through each authentication flow with concrete examples against hanzo.id.

Endpoints

EndpointURL
Discoveryhttps://hanzo.id/.well-known/openid-configuration
Authorizationhttps://hanzo.id/oauth/authorize
Tokenhttps://hanzo.id/oauth/token
UserInfohttps://hanzo.id/oauth/userinfo
JWKShttps://hanzo.id/.well-known/jwks
Introspectionhttps://hanzo.id/oauth/introspect
Revocationhttps://hanzo.id/oauth/revoke

Authorization Code Flow

The standard flow for web applications with a backend server.

┌──────┐     ┌────────┐     ┌──────────┐
│ User │     │  App   │     │ hanzo.id │
└──┬───┘     └───┬────┘     └────┬─────┘
   │  Click      │               │
   │  Login      │               │
   │────────────>│               │
   │             │  /authorize   │
   │             │──────────────>│
   │             │               │
   │  Login page │               │
   │<────────────────────────────│
   │             │               │
   │  Credentials│               │
   │────────────────────────────>│
   │             │               │
   │             │  ?code=xxx    │
   │             │<──────────────│
   │             │               │
   │             │  POST /token  │
   │             │  code + secret│
   │             │──────────────>│
   │             │               │
   │             │  access_token │
   │             │  id_token     │
   │             │<──────────────│
   │             │               │
   │  Logged in  │               │
   │<────────────│               │

Step 1: Redirect to Authorization

GET https://hanzo.id/oauth/authorize
  ?client_id=app-myapp
  &redirect_uri=https://myapp.com/callback
  &response_type=code
  &scope=openid profile email
  &state=random-csrf-token

Step 2: Exchange Code for Tokens

curl -X POST https://hanzo.id/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "app-myapp",
    "client_secret": "YOUR_CLIENT_SECRET",
    "code": "AUTHORIZATION_CODE",
    "redirect_uri": "https://myapp.com/callback"
  }'

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "scope": "openid profile email"
}

Step 3: Get User Info

curl https://hanzo.id/oauth/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"

Authorization Code + PKCE

For single-page apps (SPAs) and mobile apps that cannot securely store a client secret. Hanzo IAM supports S256 PKCE.

Step 1: Generate Code Verifier and Challenge

// Generate a random code verifier
const codeVerifier = crypto.randomUUID() + crypto.randomUUID()

// Create S256 challenge
const encoder = new TextEncoder()
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(codeVerifier))
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
  .replace(/\+/g, '-')
  .replace(/\//g, '_')
  .replace(/=+$/, '')

Step 2: Redirect with Challenge

GET https://hanzo.id/oauth/authorize
  ?client_id=app-myapp
  &redirect_uri=https://myapp.com/callback
  &response_type=code
  &scope=openid profile email
  &state=random-csrf-token
  &code_challenge=CODE_CHALLENGE
  &code_challenge_method=S256

Step 3: Exchange Code with Verifier

curl -X POST https://hanzo.id/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "app-myapp",
    "code": "AUTHORIZATION_CODE",
    "redirect_uri": "https://myapp.com/callback",
    "code_verifier": "ORIGINAL_CODE_VERIFIER"
  }'

No client secret is required when using PKCE.

Client Credentials Flow

For service-to-service authentication. No user interaction -- the application authenticates directly with its client ID and secret.

curl -X POST https://hanzo.id/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "app-myservice",
    "client_secret": "YOUR_CLIENT_SECRET",
    "scope": "openid"
  }'

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 7200,
  "scope": "openid"
}

Use Case: Service-to-Service

Every Hanzo service authenticates to other services via client credentials:

// Go service authenticating to another Hanzo service
token, err := iamClient.ClientCredentials(ctx, &oauth2.Config{
    ClientID:     "app-platform",
    ClientSecret: os.Getenv("IAM_CLIENT_SECRET"),
    TokenURL:     "https://hanzo.id/oauth/token",
    Scopes:       []string{"openid"},
})

// Use token to call another service
req.Header.Set("Authorization", "Bearer "+token.AccessToken)

Device Code Flow

For devices with limited input (CLI tools, smart TVs, IoT). The user authorizes on a separate device.

Step 1: Request Device Code

curl -X POST https://hanzo.id/oauth/device/code \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "app-cli",
    "scope": "openid profile email"
  }'

Response:

{
  "device_code": "GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://hanzo.id/device",
  "verification_uri_complete": "https://hanzo.id/device?user_code=WDJB-MJHT",
  "expires_in": 1800,
  "interval": 5
}

Step 2: Display Code to User

Show the user:

  • Go to https://hanzo.id/device
  • Enter code: WDJB-MJHT

Step 3: Poll for Token

# Poll every 5 seconds until user authorizes
curl -X POST https://hanzo.id/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "client_id": "app-cli",
    "device_code": "GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS"
  }'

Returns authorization_pending until the user approves, then returns the token.

Token Refresh

Exchange a refresh token for a new access token:

curl -X POST https://hanzo.id/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "client_id": "app-myapp",
    "client_secret": "YOUR_CLIENT_SECRET",
    "refresh_token": "REFRESH_TOKEN"
  }'

JWT Token Structure

Hanzo IAM tokens are RS256-signed JWTs. The access_token and id_token contain the same payload:

{
  "iss": "https://hanzo.id",
  "sub": "user-id",
  "aud": "app-myapp",
  "exp": 1740007200,
  "iat": 1740000000,
  "owner": "hanzo",
  "name": "john",
  "displayName": "John Doe",
  "email": "john@example.com",
  "avatar": "https://...",
  "isAdmin": false,
  "type": "normal-user"
}

Key Claims

ClaimDescription
ownerOrganization name -- scope all data queries to this value
nameUsername within the organization
subGlobally unique user ID
audApplication that issued the token
isAdminWhether user has admin privileges
typeUser type: normal-user, machine, etc.

Token Verification

Verify tokens using the JWKS endpoint:

import { createRemoteJWKSet, jwtVerify } from 'jose'

const jwks = createRemoteJWKSet(
  new URL('https://hanzo.id/.well-known/jwks')
)

const { payload } = await jwtVerify(token, jwks, {
  issuer: 'https://hanzo.id',
  audience: 'app-myapp'
})

// Scope queries to the user's organization
const org = payload.owner // e.g., 'hanzo'

Scopes

ScopeDescription
openidRequired. Returns sub claim
profileUser profile (name, displayName, avatar)
emailEmail address and verification status
phonePhone number
addressUser location
offline_accessInclude refresh token in response

Which Flow Should I Use?

Application TypeFlowPKCE
Web app (server-side)Authorization CodeOptional
SPA (React, Vue, Next.js)Authorization Code + PKCERequired
Mobile app (iOS, Android)Authorization Code + PKCERequired
CLI toolDevice CodeN/A
Service / API / BotClient CredentialsN/A
Machine identityClient CredentialsN/A

Discovery endpoints and metadata

OAuth2-specific configuration

Passwordless authentication with FIDO2

Token format and lifecycle details

How is this guide?

Last updated on

On this page