OpenID Connect Explained: A Developer’s Guide with Keycloak

Guilliano Molaire Guilliano Molaire Updated May 8, 2026 9 min read

Last updated: March 2026

Introduction

OpenID Connect (OIDC) is the authentication protocol that sits on top of OAuth 2.0. While OAuth 2.0 handles authorization (“what can this app access?”), OIDC answers the identity question (“who is this user?”). Nearly every modern single sign-on implementation uses OIDC under the hood, and Keycloak is one of the most complete OIDC providers available.

This guide breaks down OIDC from a developer’s perspective. We cover how it extends OAuth 2.0, the difference between ID tokens and access tokens, standard claims and scopes, the discovery endpoint, the UserInfo endpoint, and session management. Each section includes practical curl commands against a Keycloak instance so you can follow along.

If you are already familiar with OIDC basics and want to jump into specific integrations, check our guides for React/Next.js, Spring Boot, Django, or NestJS.

How OIDC Extends OAuth 2.0

OAuth 2.0 was designed for delegated authorization. A user grants an application permission to access their data on another service. The protocol issues access tokens, but it never standardized how to represent the user’s identity.

OIDC fills that gap by adding:

  1. ID Token: A JWT that contains claims about the authenticated user.
  2. UserInfo Endpoint: An API to fetch user profile information.
  3. Standard Scopes: openid, profile, email, address, phone.
  4. Discovery: A well-known endpoint that publishes all provider metadata.
  5. Session Management: Mechanisms for logout and session state.

The relationship is simple: every OIDC interaction is also a valid OAuth 2.0 interaction, with the openid scope included in the request.

OAuth 2.0:  Authorization framework (access tokens)
   +
OIDC:       Identity layer (ID tokens, user claims, discovery)
   =
Complete authentication and authorization

The OIDC Discovery Endpoint

Every OIDC provider publishes a JSON document at a well-known URL that describes its capabilities. This is the starting point for any OIDC integration.

For Keycloak:

curl -s "http://localhost:8080/realms/my-realm/.well-known/openid-configuration" | jq .

The response includes:

{
  "issuer": "http://localhost:8080/realms/my-realm",
  "authorization_endpoint": "http://localhost:8080/realms/my-realm/protocol/openid-connect/auth",
  "token_endpoint": "http://localhost:8080/realms/my-realm/protocol/openid-connect/token",
  "userinfo_endpoint": "http://localhost:8080/realms/my-realm/protocol/openid-connect/userinfo",
  "jwks_uri": "http://localhost:8080/realms/my-realm/protocol/openid-connect/certs",
  "end_session_endpoint": "http://localhost:8080/realms/my-realm/protocol/openid-connect/logout",
  "introspection_endpoint": "http://localhost:8080/realms/my-realm/protocol/openid-connect/token/introspect",
  "grant_types_supported": [
    "authorization_code",
    "client_credentials",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code",
    "urn:ietf:params:oauth:grant-type:token-exchange"
  ],
  "response_types_supported": ["code", "id_token", "code id_token"],
  "scopes_supported": ["openid", "profile", "email", "address", "phone", "offline_access"],
  "claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name", "preferred_username", "given_name", "family_name"],
  "id_token_signing_alg_values_supported": ["RS256", "ES256", "PS256"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "private_key_jwt"]
}

Client libraries (like openid-client for Node.js or Spring Security) use this document to automatically configure themselves. You should never hardcode endpoint URLs.

For a deeper look at dynamic discovery and client registration, see our guide on OIDC discovery and dynamic registration.

ID Tokens vs Access Tokens

This is one of the most commonly confused aspects of OIDC.

ID Token

The ID token is for the client application. It tells the app who the user is.

  • Format: Always a JWT (JSON Web Token)
  • Audience: The client application (aud claim = client ID)
  • Purpose: Authenticate the user to the client
  • Should be sent to APIs: No
{
  "iss": "http://localhost:8080/realms/my-realm",
  "sub": "f1b2c3d4-5678-90ab-cdef-1234567890ab",
  "aud": "my-app",
  "exp": 1714340000,
  "iat": 1714336400,
  "auth_time": 1714336400,
  "nonce": "abc123",
  "acr": "1",
  "azp": "my-app",
  "session_state": "session-uuid-here",
  "at_hash": "abc123hash",
  "sid": "session-id",
  "email_verified": true,
  "name": "Jane Developer",
  "preferred_username": "jane",
  "given_name": "Jane",
  "family_name": "Developer",
  "email": "[email protected]"
}

Access Token

The access token is for the resource server (API). It tells the API what the bearer is allowed to do.

  • Format: Can be JWT or opaque (Keycloak uses JWT by default)
  • Audience: The resource server(s)
  • Purpose: Authorize access to protected resources
  • Should be sent to APIs: Yes
{
  "iss": "http://localhost:8080/realms/my-realm",
  "sub": "f1b2c3d4-5678-90ab-cdef-1234567890ab",
  "aud": "account",
  "exp": 1714336700,
  "iat": 1714336400,
  "azp": "my-app",
  "scope": "openid email profile",
  "realm_access": {
    "roles": ["user", "developer"]
  },
  "resource_access": {
    "my-app": {
      "roles": ["app-admin"]
    }
  }
}

Use the JWT Token Analyzer to decode both token types and compare their claims side by side.

Key Differences Summarized

Aspect ID Token Access Token
Intended for Client application Resource server (API)
Contains identity Yes (name, email, etc.) Minimal (sub, roles)
Format Always JWT JWT or opaque
Send to APIs Never Always
Validated by Client API / resource server
Contains nonce Yes (CSRF protection) No

Scopes and Claims

OIDC defines standard scopes that control which claims appear in tokens and the UserInfo response.

Standard Scopes

Scope Claims Returned
openid sub (required for OIDC)
profile name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
email email, email_verified
address address (structured JSON object)
phone phone_number, phone_number_verified
offline_access Issues a refresh token for offline use

Requesting Scopes

Include scopes in the authorization request:

# Authorization Code Flow - redirect the user to:
http://localhost:8080/realms/my-realm/protocol/openid-connect/auth?
  client_id=my-app&
  response_type=code&
  scope=openid%20profile%20email&
  redirect_uri=http://localhost:3000/callback&
  state=random-state-value&
  nonce=random-nonce-value

After the user authenticates, exchange the authorization code for tokens:

curl -s -X POST 
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=authorization_code" 
  -d "client_id=my-app" 
  -d "client_secret=my-secret" 
  -d "code=AUTHORIZATION_CODE_HERE" 
  -d "redirect_uri=http://localhost:3000/callback" | jq .

The response includes both tokens:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzUxMiIs...",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "not-before-policy": 0,
  "session_state": "session-uuid",
  "scope": "openid profile email"
}

Custom Claims in Keycloak

Keycloak lets you add custom claims via Protocol Mappers. Common use cases:

  • User Attribute Mapper: Maps a user attribute to a token claim
  • Group Membership Mapper: Adds group names to the token
  • Hardcoded Claim Mapper: Adds a static value
  • Script Mapper: Custom JavaScript logic

To add a custom claim for department:

  1. Go to Client Scopes > my-scope > Mappers > Create.
  2. Choose User Attribute mapper.
  3. Set User Attribute: department, Token Claim Name: department.
  4. Enable Add to ID token and Add to access token.

For details on attribute mapping during identity brokering, see our guide on attribute mapping in Keycloak.

The UserInfo Endpoint

The UserInfo endpoint returns claims about the authenticated user. It is an OAuth 2.0 protected resource that requires a valid access token:

curl -s 
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/userinfo" | jq .

Response:

{
  "sub": "f1b2c3d4-5678-90ab-cdef-1234567890ab",
  "email_verified": true,
  "name": "Jane Developer",
  "preferred_username": "jane",
  "given_name": "Jane",
  "family_name": "Developer",
  "email": "[email protected]"
}

When to Use UserInfo vs ID Token

Use the ID token when:

  • You need identity information immediately after authentication
  • You want to avoid an extra HTTP request
  • The client can verify JWT signatures

Use the UserInfo endpoint when:

  • You need the freshest user data (ID tokens reflect state at issuance time)
  • Your access token is opaque (not a JWT)
  • You want to retrieve claims that were not included in the ID token

Token Validation

Every API that receives a Keycloak access token must validate it. Here is what to check:

1. Signature Verification

Fetch Keycloak’s public keys from the JWKS endpoint and verify the token signature:

# Fetch the JWKS
curl -s "http://localhost:8080/realms/my-realm/protocol/openid-connect/certs" | jq .

2. Standard Claim Validation

1. iss (issuer)   → Must match your Keycloak realm URL
2. aud (audience) → Must include your client/resource ID
3. exp (expiry)   → Must be in the future
4. iat (issued at)→ Must be in the past
5. nbf (not before) → Must be in the past (if present)

3. Keycloak-Specific Claims

6. azp (authorized party) → The client that requested the token
7. realm_access.roles      → Realm-level roles
8. resource_access          → Client-specific roles
9. scope                    → Granted scopes

For a comprehensive validation guide, see our post on Keycloak token validation for APIs.

Token Introspection (Opaque Tokens)

If you use opaque tokens or need real-time token status (checking if a token has been revoked), use the introspection endpoint:

curl -s -X POST 
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/token/introspect" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "token=${ACCESS_TOKEN}" 
  -d "client_id=my-resource-server" 
  -d "client_secret=resource-server-secret" | jq .

Response:

{
  "active": true,
  "sub": "f1b2c3d4-5678-90ab-cdef-1234567890ab",
  "email_verified": true,
  "iss": "http://localhost:8080/realms/my-realm",
  "typ": "Bearer",
  "preferred_username": "jane",
  "given_name": "Jane",
  "family_name": "Developer",
  "client_id": "my-app",
  "username": "jane",
  "token_type": "Bearer",
  "active": true,
  "scope": "openid email profile",
  "exp": 1714336700
}

The critical field is "active": true. If the token has been revoked or expired, this will be false.

OIDC Flows

OIDC supports several authentication flows, each suited to different application types:

Authorization Code Flow (Recommended)

Best for: Web applications with a backend, mobile apps, SPAs (with PKCE).

1. App redirects user to Keycloak /auth endpoint
2. User authenticates
3. Keycloak redirects back with authorization code
4. App exchanges code for tokens at /token endpoint

Always use PKCE (Proof Key for Code Exchange) to prevent authorization code interception. See our PKCE guide for implementation details.

Client Credentials Flow

Best for: Machine-to-machine communication, background services, AI agents.

curl -s -X POST 
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" 
  -d "grant_type=client_credentials" 
  -d "client_id=my-service" 
  -d "client_secret=service-secret" 
  -d "scope=openid"

No user is involved. The token represents the service itself.

Device Authorization Flow

Best for: Smart TVs, CLI tools, IoT devices with limited input capability.

See our Device Flow guide for a complete walkthrough.

Implicit Flow (Deprecated)

The Implicit flow returned tokens directly in the URL fragment. This is no longer recommended due to security risks. Use Authorization Code + PKCE instead. The upcoming OAuth 2.1 specification formally removes it.

Session Management

OIDC includes mechanisms for managing user sessions across applications.

Session State

When Keycloak issues tokens, it includes a session_state claim. Applications can use this to detect session changes via a hidden iframe that checks the session status periodically.

Logout

OIDC defines several logout mechanisms:

RP-Initiated Logout (front-channel):

# Redirect the user to:
http://localhost:8080/realms/my-realm/protocol/openid-connect/logout?
  id_token_hint=${ID_TOKEN}&
  post_logout_redirect_uri=http://localhost:3000&
  client_id=my-app

Back-Channel Logout: Keycloak sends a POST request to each registered client’s backchannel logout URL when a user logs out. Configure this in the client settings under Logout settings.

For applications with strict session requirements, Skycloak’s session management features provide visibility into active sessions across all connected applications.

Putting It All Together

Here is a complete flow tested against Keycloak:

#!/bin/bash
KEYCLOAK="http://localhost:8080"
REALM="my-realm"
CLIENT_ID="my-app"
CLIENT_SECRET="my-secret"
REDIRECT_URI="http://localhost:3000/callback"

# 1. Discover endpoints
echo "=== Discovery ==="
DISCOVERY=$(curl -s "${KEYCLOAK}/realms/${REALM}/.well-known/openid-configuration")
TOKEN_ENDPOINT=$(echo $DISCOVERY | jq -r '.token_endpoint')
USERINFO_ENDPOINT=$(echo $DISCOVERY | jq -r '.userinfo_endpoint')
echo "Token endpoint: ${TOKEN_ENDPOINT}"
echo "UserInfo endpoint: ${USERINFO_ENDPOINT}"

# 2. Get tokens (using password grant for testing)
echo ""
echo "=== Token Request ==="
TOKEN_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" 
  -d "grant_type=password" 
  -d "client_id=${CLIENT_ID}" 
  -d "client_secret=${CLIENT_SECRET}" 
  -d "username=jane" 
  -d "password=jane123" 
  -d "scope=openid profile email")

ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
ID_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.id_token')

echo "Access token (first 50 chars): ${ACCESS_TOKEN:0:50}..."
echo "ID token (first 50 chars): ${ID_TOKEN:0:50}..."

# 3. Decode the ID token (base64 decode the payload)
echo ""
echo "=== ID Token Claims ==="
echo $ID_TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .

# 4. Call UserInfo
echo ""
echo "=== UserInfo ==="
curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" 
  "${USERINFO_ENDPOINT}" | jq .

# 5. Introspect the access token
echo ""
echo "=== Token Introspection ==="
curl -s -X POST 
  "${KEYCLOAK}/realms/${REALM}/protocol/openid-connect/token/introspect" 
  -d "token=${ACCESS_TOKEN}" 
  -d "client_id=${CLIENT_ID}" 
  -d "client_secret=${CLIENT_SECRET}" | jq .

Common Pitfalls

Sending the ID token to APIs. The ID token is meant for the client. Use the access token for API calls.

Not validating the nonce. In the Authorization Code flow, always send a nonce parameter and verify it appears in the ID token. This prevents replay attacks.

Hardcoding endpoints. Always use the discovery document to resolve endpoint URLs. Keycloak may change paths between versions.

Ignoring aud validation. Always check that the aud claim matches your expected audience. A token issued for one client should not be accepted by another.

Using long-lived access tokens. Keep access tokens short-lived (5-15 minutes) and use refresh tokens for renewal. See our guide on JWT best practices for token lifecycle management.

Further Reading

Conclusion

OIDC takes OAuth 2.0’s authorization framework and adds a complete identity layer. Understanding the distinction between ID tokens and access tokens, using standard scopes, leveraging the discovery endpoint, and properly validating tokens are the foundation of any secure OIDC integration.

Keycloak implements the full OIDC specification plus extensions like token exchange, device flow, and fine-grained authorization. Whether you are building a single-page app, a microservices architecture, or an AI agent system, these OIDC fundamentals remain the same.

Want a production-ready OIDC provider without the infrastructure burden? Try Skycloak free and get a fully managed Keycloak instance with built-in monitoring and enterprise SLA.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman