DAY 07 WEEK 2: CORE SERVICES

Identity Provider (IdP) - Hands-on

Create OIDC clients, perform OAuth2 flows with curl/Postman, and introspect tokens. This is where theory becomes muscle memory.

Yesterday you learned what OAuth2 and OIDC are. Today, you'll make them real. By the end of this session, you'll have registered your own OAuth client, obtained tokens through multiple flows, and verified their contents—all using nothing but curl commands. This hands-on experience is what separates developers who understand identity from those who just copy-paste examples and pray.

📋Learning Objectives

  • Create an OIDC client via the Admin UI
  • Perform Authorization Code flow
  • Perform Client Credentials flow
  • Introspect and validate tokens

🗺️OAuth Flows: The Big Picture

Before diving into commands, let's visualize what we're about to do. Today you'll execute two different OAuth flows. Understanding the difference will help you choose the right one for your use case—and debug when things go wrong.

Flow 1: Client Credentials (Machine-to-Machine)

The simplest flow—no user involved. Your service proves its identity and gets a token.

Client Credentials Flow
sequenceDiagram participant Service as 🤖 Your Service participant IdP as 🔐 IdP participant API as 📡 Protected API Service->>IdP: POST /token
client_id + client_secret
grant_type=client_credentials IdP->>IdP: Validate client credentials IdP-->>Service: Access Token (JWT) Service->>API: GET /api/data
Authorization: Bearer [token] API-->>Service: 200 OK + Data
📋 When to Use
  • Background jobs and cron tasks
  • Service-to-service communication
  • APIs calling other APIs
  • Any scenario without a human user

Flow 2: Authorization Code (User Login)

The standard flow for user-facing apps. Involves browser redirects and user consent.

Authorization Code Flow
sequenceDiagram participant User as 👤 User participant App as 🌐 Your App participant IdP as 🔐 IdP participant API as 📡 Protected API User->>App: Click "Login" App->>IdP: Redirect to /authorize
response_type=code IdP->>User: Show Login Page User->>IdP: Enter credentials IdP->>User: Show Consent Screen User->>IdP: Grant permissions IdP->>App: Redirect with code App->>IdP: POST /token
code + client_secret IdP-->>App: Access Token + ID Token + Refresh Token App->>API: GET /api/data
Authorization: Bearer [token] API-->>App: 200 OK + Data App->>User: Show data
📋 When to Use
  • Web applications with user login
  • Mobile apps (with PKCE)
  • Any scenario where a human user is present
  • When you need to act on behalf of a user

Key Differences at a Glance

Client Credentials

  • Single HTTP request
  • No browser involved
  • No user consent
  • Access token only
  • Token represents the client

Authorization Code

  • Multiple HTTP requests
  • Browser redirects required
  • User consent screen
  • Access + ID + Refresh tokens
  • Token represents the user

🔧Exercise 1: Create an OIDC Client

Before your application can participate in OAuth flows, it needs an identity. Think of client registration like getting a driver's license for your app—it tells the IdP who you are, what permissions you need, and where to send users back after login. Without a registered client, your app is a stranger the IdP won't talk to.

Option A: Using the Experience App (OAuth Client Center)

The Experience App provides a user-friendly interface for managing OAuth clients:

Creating a New Client

  1. Open https://experience.self.empowernow.ai
  2. Log in with your credentials
  3. Navigate to Identity → OAuth Client Center in the navigation menu
  4. Click Register New Client
  5. Fill in the registration form:
    • Client Name: Training Test Client
    • Application Type: Confidential (for server-side apps)
    • Redirect URIs: http://localhost:3000/callback
    • Grant Types: authorization_code, client_credentials, refresh_token
    • Scopes: openid, profile, email
  6. Click Register and copy the client_id and client_secret

Editing an Existing Client

The same form UI is used for editing existing clients:

  1. From the OAuth Client Center, click on any client to open its detail view
  2. You'll see the full client configuration including:
    • CLIENT ID - Copy button for easy use in your application
    • Client configuration JSON - Shows grant types, scopes, redirect URIs, etc.
  3. Modify any settings using the same form interface
  4. Use the action buttons:
    • Export - Download client configuration
    • Clone - Create a copy of this client
    • Rotate Secret - Generate a new client secret
    • Delete - Remove the client
    • Save Changes - Persist your modifications
💡 Direct Client URL

You can navigate directly to a client's edit page using: https://experience.self.empowernow.ai/idp/clients-center/{client_id}

Option B: Using Postman (Recommended for API Testing)

Postman is excellent for testing OAuth flows. You can register clients directly via the DCR (Dynamic Client Registration) API:

💡 Postman OAuth Client Plugin

Install the EmpowerNow OAuth Client Postman collection from the team shared workspace. It includes pre-configured requests for client registration, token flows, and introspection.

  1. Open Postman and create a new POST request
  2. Set the URL to: https://idp.self.empowernow.ai/api/oidc/register
  3. Set Content-Type header to application/json
  4. Add this JSON body:
{
  "client_name": "Training Test Client",
  "application_type": "web",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code", "client_credentials", "refresh_token"],
  "response_types": ["code"],
  "scope": "openid profile email",
  "token_endpoint_auth_method": "client_secret_post"
}

Click Send. The response contains your new client credentials:

{
  "client_id": "generated-client-id",
  "client_secret": "generated-secret",
  "client_name": "Training Test Client",
  "registration_access_token": "...",
  ...
}

Option C: Use Pre-seeded Clients (Quick Start)

For training purposes, you can use clients already seeded in the database:

Confidential Client (for client_credentials):

  • client_id: bff-client
  • client_secret: secret1

Public Client (for authorization_code):

  • client_id: idp-ui-client
  • No secret required (public client)
⚠️ Save Credentials

If you register a new client, copy the client_secret immediately! It's only shown once during registration. Store it securely for the exercises below.

🔄Exercise 2: Client Credentials Flow

Imagine a cron job that needs to call your API at midnight. There's no user sitting at a keyboard—it's machine talking to machine. The client credentials flow is how services authenticate themselves: "I am the Scheduler Service, here's my proof, give me a token." It's the simplest OAuth flow because there's no user consent, no redirects, just credentials in, token out.

This flow is for machine-to-machine authentication (no user involved):

# Replace with your client_id and client_secret
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"

# Request access token
curl -k -X POST https://idp.self.empowernow.ai/api/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "scope=openid"

Expected Response

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid"
}
💡 PowerShell Alternative
$body = @{
    grant_type = "client_credentials"
    client_id = "your-client-id"
    client_secret = "your-client-secret"
    scope = "openid"
}
Invoke-RestMethod -Uri "https://idp.self.empowernow.ai/api/oidc/token" `
    -Method POST -Body $body -SkipCertificateCheck
🧪 Try It: Client Credentials Flow Interactive

Copy curl and run in your terminal to execute the client credentials flow using the pre-seeded bff-client. This will get you a real access token!

✅ What to Expect

A JSON response with access_token, token_type, expires_in, and scope. Copy the access_token value—you'll need it for the introspection exercise!

👤Exercise 3: Authorization Code Flow

This is the flow you experience every time you click "Sign in with Google." A user is involved—they see a login page, enter credentials, consent to permissions. The magic is that your app never sees their password. Instead, you get a one-time authorization code that you exchange for tokens. It's more steps, but it's how you handle real humans securely.

This flow involves user authentication. It's a multi-step process:

Step 1: Generate Authorization URL

# Build authorization URL
CLIENT_ID="your-client-id"
REDIRECT_URI="http://localhost:3000/callback"
STATE="random-state-string"
NONCE="random-nonce-string"

# Open this URL in browser
echo "https://idp.self.empowernow.ai/api/oidc/authorize?\
response_type=code&\
client_id=$CLIENT_ID&\
redirect_uri=$REDIRECT_URI&\
scope=openid%20profile%20email&\
state=$STATE&\
nonce=$NONCE"

Step 2: User Logs In

  1. Open the URL in browser
  2. Log in with test credentials
  3. Consent to requested scopes
  4. Browser redirects to redirect_uri with code parameter

Step 3: Exchange Code for Tokens

# Extract code from redirect URL query params
CODE="authorization-code-from-redirect"

# Exchange code for tokens
curl -k -X POST https://idp.self.empowernow.ai/api/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "code=$CODE" \
  -d "redirect_uri=$REDIRECT_URI"

Response with All Token Types

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

🔍Exercise 4: Token Introspection

You've got a token. But is it still valid? Was it revoked? What's actually in it? Token introspection is like asking the IdP: "Hey, tell me everything about this token." Unlike JWT decoding (which happens locally), introspection calls the IdP—useful when you need real-time revocation checks or when dealing with opaque tokens.

Introspection allows you to verify if a token is valid and get its claims:

# Introspect an access token
ACCESS_TOKEN="your-access-token"

curl -k -X POST https://idp.self.empowernow.ai/api/oidc/token/introspect \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$ACCESS_TOKEN" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET"

Active Token Response

{
  "active": true,
  "sub": "user-uuid-123",
  "client_id": "your-client-id",
  "scope": "openid profile email",
  "exp": 1699900000,
  "iat": 1699896400,
  "iss": "https://idp.self.empowernow.ai"
}

Expired/Invalid Token Response

{
  "active": false
}
🔬 Try It: Introspect Your Token Interactive

Copy curl and run in your terminal. Replace YOUR_ACCESS_TOKEN in the curl command with the access token from the Client Credentials exercise. The IdP will return the token details.

✅ What to Expect

If the token is valid, you'll see "active": true along with all claims: sub, client_id, scope, exp, iss. If the token is expired or invalid, you'll see "active": false.

🔄Exercise 5: Refresh Tokens

Access tokens expire—typically in an hour. Imagine asking users to log in again every 60 minutes. That's terrible UX. Refresh tokens solve this elegantly: your app exchanges an expiring access token for a fresh one, silently, in the background. The user stays logged in for days without ever seeing another login prompt.

Use a refresh token to get a new access token:

# Use refresh token to get new access token
REFRESH_TOKEN="your-refresh-token"

curl -k -X POST https://idp.self.empowernow.ai/api/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "refresh_token=$REFRESH_TOKEN"
📝 Token Rotation

EmpowerNow IdP supports refresh token rotation. Each refresh returns a new refresh token, and the old one is invalidated. This improves security.

👁️Exercise 6: UserInfo Endpoint

Sometimes you need more than what's in the ID token. The UserInfo endpoint returns the user's profile data—name, email, profile picture—based on the scopes in your access token. It's the OIDC standard way to fetch user attributes after authentication.

Get user profile information using an access token:

# Get user info
ACCESS_TOKEN="your-access-token"

curl -k -X GET https://idp.self.empowernow.ai/api/oidc/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "sub": "user-uuid-123",
  "name": "John Doe",
  "email": "john.doe@empowerid.com",
  "email_verified": true,
  "preferred_username": "johndoe"
}
⚠️ Important

The UserInfo endpoint requires a token obtained via the Authorization Code flow (with a user login). Client credentials tokens don't have user information because there's no user involved. You'll need to complete Exercise 3 first to test this endpoint.

🔬Hands-on Labs

Now let's put it all together with structured labs. Each lab builds on the previous one, giving you a complete end-to-end understanding of OAuth flows in action.
LAB 1 The Complete Client Credentials Journey ⏱️ 10 minutes

Let's trace through the entire client credentials flow, from token request to API call.

1

Request an access token:

TOKEN_RESPONSE=$(curl -s -X POST https://idp.self.empowernow.ai/api/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=bff-client" \
  -d "client_secret=secret1" \
  -d "scope=openid profile")

echo $TOKEN_RESPONSE | jq .
2

Extract the access token:

ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
echo "Token: ${ACCESS_TOKEN:0:50}..."
3

Decode the token header to see the algorithm:

echo $ACCESS_TOKEN | cut -d'.' -f1 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
4

Decode the payload to see the claims:

echo $ACCESS_TOKEN | cut -d'.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
5

Introspect the token to verify it's active:

curl -s -X POST https://idp.self.empowernow.ai/api/oidc/token/introspect \
  -d "token=$ACCESS_TOKEN" \
  -d "client_id=bff-client" \
  -d "client_secret=secret1" | jq .
✅ Expected Output

The introspection should show "active": true with claims matching the decoded payload. Note that sub in a client credentials token is typically the client_id, not a user ID.

🤔 Self-Check
  • What's the difference between decoding and introspecting?
  • Why doesn't this token have a refresh_token?
  • What happens when this token expires?
LAB 2 Authorization Code Flow (Manual Walkthrough) ⏱️ 15 minutes

This lab walks through the authorization code flow manually, so you understand every step.

1

Build the authorization URL:

# Set variables (use your client from Exercise 1, or the pre-seeded one)
CLIENT_ID="experience-app"
REDIRECT_URI="https://experience.self.empowernow.ai/oauth/callback"
STATE="my-random-state-$(date +%s)"
NONCE="my-random-nonce-$(date +%s)"

# Build URL
AUTH_URL="https://idp.self.empowernow.ai/api/oidc/authorize?\
response_type=code&\
client_id=$CLIENT_ID&\
redirect_uri=$REDIRECT_URI&\
scope=openid%20profile%20email&\
state=$STATE&\
nonce=$NONCE"

echo "Open this URL in your browser:"
echo $AUTH_URL
2

Open the URL in browser and log in:

  • Use test credentials: alice / password
  • Review and accept the consent screen
  • Watch the browser redirect to the callback URL
3

Extract the authorization code from the URL:

After redirect, the URL looks like:

https://experience.self.empowernow.ai/oauth/callback?code=AUTH_CODE_HERE&state=my-random-state-...

Copy the code parameter value.

4

Exchange the code for tokens:

CODE="paste-your-code-here"

curl -s -X POST https://idp.self.empowernow.ai/api/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "client_id=$CLIENT_ID" \
  -d "code=$CODE" \
  -d "redirect_uri=$REDIRECT_URI" | jq .
✅ Expected Output

You should receive three tokens: access_token, id_token, and refresh_token. The id_token contains user information because a user logged in!

🤔 Self-Check
  • Why is state important for security?
  • What's the difference between this response and client credentials?
  • Decode the id_token—what user information do you see?
LAB 3 Token Refresh Cycle ⏱️ 5 minutes

Using the refresh token from Lab 2, get a fresh access token.

1

Use the refresh token from Lab 2:

REFRESH_TOKEN="paste-refresh-token-from-lab-2"

curl -s -X POST https://idp.self.empowernow.ai/api/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "client_id=experience-app" \
  -d "refresh_token=$REFRESH_TOKEN" | jq .
2

Compare the tokens:

  • Is the new access_token different from the original?
  • Is there a new refresh_token? (rotation)
  • What about expires_in?
✅ Expected Output

A new access token with a fresh expiration. If refresh token rotation is enabled, you'll also get a new refresh token—the old one is now invalid.

🔧Troubleshooting OAuth Flows

OAuth errors can be cryptic. "invalid_grant" tells you something went wrong, but what exactly? Here are three scenarios you'll definitely encounter—learn to diagnose them now.
🔑 The Invalid Client Secret Case #1
📋 Symptom

Token request returns: {"error": "invalid_client", "error_description": "Client authentication failed"}

🔍 Investigation
  • Double-check client_id spelling (case-sensitive!)
  • Verify client_secret is correct (was it rotated?)
  • Check if using Basic auth vs POST body for credentials
  • Ensure client exists: query the database directly
docker exec empowernow-db-1 psql -U postgres -d idp_db \
  -c "SELECT client_id FROM idp.oauth_clients WHERE client_id = 'your-client-id';"
✅ Fix
  • Copy credentials directly from the Admin UI or seed file
  • For confidential clients, ensure you're sending the secret
  • Check token_endpoint_auth_method matches your request format
🔗 The Redirect URI Mismatch Case #2
📋 Symptom

Authorization request fails with: {"error": "invalid_request", "error_description": "redirect_uri does not match registered URIs"}

🔍 Investigation

The redirect URI must match exactly—including:

  • Protocol: http vs https
  • Port: localhost:3000 vs localhost
  • Path: /callback vs /callback/ (trailing slash!)
  • Query parameters must not be present in registered URI
# Check registered URIs
docker exec empowernow-db-1 psql -U postgres -d idp_db \
  -c "SELECT redirect_uris FROM idp.oauth_clients WHERE client_id = 'your-client-id';"
✅ Fix
  • Use URL-encoded redirect_uri in the authorize request
  • Register all variations you might use (dev, staging, prod)
  • For local development, register http://localhost:PORT/callback
The Expired Authorization Code Case #3
📋 Symptom

Token exchange fails with: {"error": "invalid_grant", "error_description": "Authorization code has expired"}

🔍 Investigation

Authorization codes are short-lived (usually 60-300 seconds) and single-use:

  • Did you wait too long between authorization and token exchange?
  • Did you try to use the code twice? (replay attack protection)
  • Is the server time in sync?
✅ Fix
  • Exchange the code immediately after receiving it
  • Never store authorization codes—they're meant to be ephemeral
  • If the code expired, restart the authorization flow from the beginning

🌐Federation: Hands-On Labs

If your organization federates with Azure AD or another external IdP, you'll encounter federated tokens—JWTs that were minted after validating an external identity. These tokens look slightly different from local auth tokens. Let's learn to identify and debug them.

Lab: Inspect a Federated Token

In this lab, you'll decode a federated token and identify its unique characteristics.

Step 1: Get a Federated Token (Simulated)

If you don't have Azure AD federation configured, use this example token for analysis:

# Example federated token payload (decoded)
{
  "iss": "https://idp.self.empowernow.ai",
  "sub": "auth:account:entra.contoso:ffe9b9f0-ec04-4b9c-bd48-30fdefd72a5e",
  "aud": ["bff-client"],
  "email": "john.doe@contoso.com",
  "name": "John Doe",
  "roles": ["User", "Admin"],
  "idp": "entra.contoso",
  "idp_sub": "ffe9b9f0-ec04-4b9c-bd48-30fdefd72a5e",
  "federation": {
    "orig_iss": "https://login.microsoftonline.com/contoso.com/v2.0",
    "orig_sub": "ffe9b9f0-ec04-4b9c-bd48-30fdefd72a5e",
    "stable_id_claim": "oid",
    "mapped_at": "2025-01-20T10:30:00Z"
  },
  "emp_account_arn": "auth:account:entra.contoso:ffe9b9f0-ec04-4b9c-bd48-30fdefd72a5e",
  "exp": 1737456600,
  "iat": 1737453000
}

Step 2: Identify Federation Markers

Look for these claims that indicate a federated identity:

ClaimValue PatternMeaning
subauth:account:entra.*:...Provider-scoped ARN (not empowernow)
idpentra.contosoExternal IdP identifier
idp_subUUID or external IDOriginal subject from external IdP
federationObject with provenanceTracks original issuer and mapping
emp_account_arnFull ARNCanonical account identifier
💡 Local vs Federated

A local user has sub: auth:account:empowernow:alice with no federation claim. A federated user has a provider like entra.contoso in the ARN and includes the federation provenance block.

Step 3: Decode Using jwt.io

  1. Go to jwt.io
  2. Paste the complete JWT (header.payload.signature)
  3. Look at the decoded payload
  4. Find the federation.orig_iss to see which external IdP authenticated the user

Lab: Trace Federation in Logs

When troubleshooting federation issues, IdP logs contain valuable information.

View Federation Logs

# Filter IdP logs for federation events
docker logs idp-app 2>&1 | grep -i "federation\|external\|jwks"

# Expected log entries for successful federation:
# [INFO] Federation: Validating token from issuer https://login.microsoftonline.com/...
# [INFO] Federation: JWKS cache hit for entra-id
# [INFO] Federation: Token signature valid, mapping claims
# [INFO] Federation: Minted ARN auth:account:entra.contoso:ffe9b9f0...

Common Federation Errors

🔑 JWKS Validation Failed Federation Error
📋 Symptom

Log shows: Federation: Signature verification failed - no matching kid

🔍 Investigation
  • The JWT's kid (key ID) doesn't match any key in the cached JWKS
  • Azure may have rotated its signing keys
  • JWKS cache may be stale
✅ Fix
  • Clear the JWKS cache: docker exec idp-app curl -X POST localhost:8002/admin/cache/clear/jwks
  • Verify the JWKS URL is correct in federation.yaml
  • Check that the Azure tenant ID matches the configured issuer
🎯 Audience Mismatch Federation Error
📋 Symptom

Log shows: Federation: Invalid audience - expected api://xxx, got yyy

🔍 Investigation
  • The Azure token's aud claim doesn't match what's configured in federation.yaml
  • Azure tokens can use either aud or azp for audience
✅ Fix
  • Check the Azure app registration's "Application ID URI"
  • Update federation.yaml audience array to include both formats:
    audience: ["api://your-client-id", "your-client-id"]

Lab: Test PDP Authorization for Federated Users

PDP policies work the same for local and federated users—they just target different ARN patterns.

# AuthZEN request for a federated user
curl -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
  -H "Content-Type: application/json" \
  -d '{
    "subject": {
      "type": "user",
      "id": "auth:account:entra.contoso:ffe9b9f0-ec04-4b9c-bd48-30fdefd72a5e"
    },
    "action": {
      "name": "document.view"
    },
    "resource": {
      "type": "document",
      "id": "doc-123"
    }
  }'

# Example policy that permits all Azure AD users from contoso
# Policy subjects:
#   - { type: user, id: "auth:account:entra.contoso:*" }
🔗 Full Configuration Guide

For step-by-step Azure AD app registration, complete federation.yaml configuration, claims mapping rules, and advanced troubleshooting, see the official EmpowerNow documentation and your identity administrator.

Knowledge Check

Test your understanding before moving on. Click each question to reveal the answer.
1 When would you use client credentials vs authorization code flow?

Client Credentials: Use when there's no user—background jobs, service-to-service calls, scheduled tasks. The token represents the application itself.

Authorization Code: Use when a user is logging in. Involves browser redirects and consent. The token represents the user and can access their data.

2 Why is the authorization code short-lived and single-use?

The authorization code travels through the browser (in the URL), making it visible in browser history, server logs, and potentially to malicious scripts. Keeping it short-lived (60-300 seconds) and single-use minimizes the window for theft and prevents replay attacks.

The real secrets (access token, refresh token) are exchanged server-to-server via HTTPS POST, never exposed in URLs.

3 What's the purpose of the "state" parameter?

The state parameter prevents CSRF (Cross-Site Request Forgery) attacks. Your app generates a random string, includes it in the authorization request, and verifies it matches when the callback arrives.

Without state validation, an attacker could trick a user into authorizing a malicious app using the victim's session.

4 What's the difference between token introspection and JWT decoding?

JWT Decoding: Local operation—you base64-decode the token to see its claims. Works offline but can't tell you if the token was revoked.

Introspection: Calls the IdP to verify the token. Returns real-time validity (active: true/false) and can detect revocation. Required for opaque tokens or when real-time revocation checks are needed.

5 Why does token refresh exist? Why not just use long-lived access tokens?

Short-lived access tokens limit damage from theft. If an attacker steals an access token, it's only valid for an hour. Long-lived tokens would give attackers extended access.

Refresh tokens are stored securely on the backend, never sent to browsers or included in API calls. They're harder to steal than access tokens that travel with every request.

6 What tokens do you get from client credentials vs authorization code?

Client Credentials: Access token only. No refresh token (why would a machine need to refresh silently?) and no ID token (no user to identify).

Authorization Code: Access token, refresh token, and ID token. The ID token contains user claims, and the refresh token enables long sessions without re-prompting for login.

7 What does "redirect_uri mismatch" error mean?

The redirect_uri in your authorization request doesn't exactly match any URI registered for that client. OAuth requires exact matching for security—this prevents attackers from redirecting authorization codes to their own servers.

Check for: protocol mismatch (http vs https), port differences, trailing slashes, typos. The URI must match character-for-character.

📝Day 7 Checkpoint

  • Created an OIDC client via Admin UI
  • Successfully performed Client Credentials flow
  • Completed Authorization Code flow
  • Introspected a token and verified it's active
  • Refreshed an access token using refresh token
  • Retrieved user info from UserInfo endpoint