| name | oauth-oidc-protocols |
| description | OAuth 2.0 and OpenID Connect protocol fundamentals including authorization code flow with PKCE, client credentials, refresh tokens, discovery documents, JWKS, and token introspection. Protocol-level troubleshooting and compliance. |
| invocable | false |
OAuth 2.0 & OpenID Connect Protocols
When to Use This Skill
Use this skill when:
- Choosing the correct OAuth 2.0 grant type for a scenario
- Debugging token exchange flows or redirect-based authentication
- Understanding what claims and headers appear in identity tokens vs access tokens
- Implementing or troubleshooting PKCE (Proof Key for Code Exchange)
- Working with discovery documents, JWKS endpoints, or token introspection
- Reviewing security properties of different protocol flows
- Implementing refresh token rotation or token revocation
Core Principles
- OAuth 2.0 is for Authorization, OIDC is for Authentication โ OAuth alone does not tell you who the user is. OpenID Connect adds an identity layer (the ID token) on top of OAuth.
- Authorization Code + PKCE is the Universal Flow โ Use it for web apps, SPAs (via BFF), native apps, and any interactive scenario. It replaced implicit flow.
- Tokens are Opaque to Clients โ Clients should not parse access tokens. Only the resource server (API) validates access tokens. Clients use the ID token for authentication.
- Discovery Documents are the Source of Truth โ Always resolve endpoints from
/.well-known/openid-configuration rather than hardcoding URLs.
- Refresh Tokens Require Secure Storage โ Refresh tokens are long-lived credentials. Rotate them on every use (
OneTimeOnly) and store them server-side.
Related Skills
identityserver-configuration โ Server-side configuration of clients, resources, and scopes
aspnetcore-authentication โ Implementing OIDC authentication in ASP.NET Core apps
token-management โ Automated token lifecycle with Duende.AccessTokenManagement
identity-security-hardening โ Security hardening including DPoP, PAR, and FAPI
duende-bff โ Backend-for-Frontend pattern for SPAs
Docs: https://docs.duendesoftware.com/identityserver/fundamentals
Concept 1: The OAuth 2.0 / OIDC Mental Model
Roles
| Role | OAuth 2.0 Term | OIDC Term | Example |
|---|
| User | Resource Owner | End-User | A person logging in |
| Browser/App | Client | Relying Party (RP) | ASP.NET Core web app |
| Token Server | Authorization Server | OpenID Provider (OP) | Duende IdentityServer |
| API | Resource Server | โ | ASP.NET Core Web API |
Tokens
| Token | Purpose | Who Consumes It | Format |
|---|
| ID Token | Proves user identity | Client application | Always JWT |
| Access Token | Authorizes API calls | Resource server (API) | JWT or reference |
| Refresh Token | Obtains new access tokens | Client application | Opaque handle |
Critical Rule: Clients authenticate users with the ID token. Clients authorize API calls with the access token. Never use an access token to determine who a user is. Never send an ID token to an API.
Concept 2: Grant Types (Flows)
Authorization Code + PKCE (Recommended for All Interactive Scenarios)
The authorization code flow with PKCE is the recommended flow for all clients that involve a user. PKCE prevents authorization code interception attacks.
How it works:
- Client generates a random
code_verifier and its SHA256 hash code_challenge
- Client redirects user to the authorize endpoint with
code_challenge
- User authenticates at IdentityServer and consents (if required)
- IdentityServer redirects back with an authorization
code
- Client exchanges the
code + code_verifier at the token endpoint
- IdentityServer verifies the verifier matches the original challenge
- IdentityServer returns ID token + access token (+ refresh token if
offline_access scope)
โโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โClientโ โ Browser โ โIdentityServerโ
โโโโฌโโโโ โโโโโโฌโโโโโโ โโโโโโโโฌโโโโโโโโ
โ 1. Generate PKCE โ โ
โ code_verifier โ โ
โ code_challenge โ โ
โ โ โ
โ 2. Redirect โโโโโโโโโโโโโโโโโโโโโโโโโโโโบ โ
โ /authorize?code_challenge=... โ
โ โ 3. User logs in โ
โ โ โโโโโโโโโโโโโโโโโโโโบ โ
โ โ โ
โ 4. Redirect back โโโโโโโโโโโโโโโโโโโโโโโ โ
โ ?code=abc123 โ โ
โ โ โ
โ 5. POST /token โโโโโโโโโโโโโโโโโโโโโโโโโโบโ
โ code=abc123&code_verifier=... โ
โ โ โ
โ 6. Tokens โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ { id_token, access_token, โ
โ refresh_token } โ
โโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโ
When to use: Web applications, native apps, SPAs (via BFF pattern).
Client Credentials
For machine-to-machine communication with no user involvement.
โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Service โ โIdentityServerโ
โโโโโโฌโโโโโโ โโโโโโโโฌโโโโโโโโ
โ POST /token โ
โ grant_type=client_credentials โ
โ client_id=... โ
โ client_secret=... โ
โ scope=api1 โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโบ โ
โ โ
โ { access_token } โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
When to use: Background services, daemons, server-to-server API calls.
Key difference: No user identity โ the access token contains only client claims, not user claims.
Refresh Token Exchange
โโโโโโโโ โโโโโโโโโโโโโโโโ
โClientโ โIdentityServerโ
โโโโฌโโโโ โโโโโโโโฌโโโโโโโโ
โ POST /token โ
โ grant_type=refresh_token โ
โ refresh_token=old_rt โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ โ
โ โ
โ { access_token, โ
โ refresh_token: new_rt } โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
With RefreshTokenUsage = OneTimeOnly, each refresh returns a new refresh token. The old one is invalidated. This enables refresh token rotation, a key security measure. Note: the default changed to ReUse in IdentityServer v7.0 โ set OneTimeOnly explicitly for rotation.
Deprecated / Discouraged Flows
| Flow | Status | Why |
|---|
Implicit (token / id_token) | Deprecated | Tokens in URL fragments; no PKCE protection |
| Resource Owner Password (ROPC) | Discouraged | Client handles credentials directly; no MFA support |
| Hybrid | Replaced | Use Authorization Code + PKCE instead |
Concept 3: Discovery and JWKS
Discovery Document
Every OpenID Connect provider publishes a discovery document at /.well-known/openid-configuration. This JSON document advertises:
- Endpoint URLs (authorize, token, userinfo, introspection, revocation, end_session)
- Supported grant types, scopes, claims, and response types
- Signing algorithms and JWKS URI
- Token endpoint authentication methods
using var httpClient = new HttpClient();
var disco = await httpClient.GetDiscoveryDocumentAsync("https://identity.example.com");
if (disco.IsError) throw new Exception(disco.Error);
var tokenEndpoint = disco.TokenEndpoint;
var jwksUri = disco.JwksUri;
Best Practice: Never hardcode endpoint URLs. Always resolve them from the discovery document. This ensures your application adapts to URL changes and load balancer configurations.
JWKS (JSON Web Key Set)
The JWKS endpoint (advertised via the jwks_uri field in the discovery document; in Duende IdentityServer this is /.well-known/openid-configuration/jwks) publishes the public keys used to verify token signatures. APIs and clients fetch this to validate JWTs.
Key rotation: When IdentityServer rotates signing keys, the new key appears in JWKS during the propagation period before it becomes the active signing key. Client libraries cache JWKS for 24 hours by default.
Concept 4: Token Anatomy
ID Token (JWT)
{
"iss": "https://identity.example.com",
"sub": "818727",
"aud": "web.app",
"exp": 1311281970,
"iat": 1311280970,
"nonce": "n-0S6_WzA2Mj",
"auth_time": 1311280969,
"at_hash": "77QmUPtjPfzWtF2AnpK9RQ",
"amr": ["pwd", "mfa"],
"name": "Alice Smith",
"email": "alice@example.com"
}
Key claims:
iss โ Issuer (must match your IdentityServer URL)
sub โ Subject (unique user identifier)
aud โ Audience (must match the client ID)
nonce โ Replay protection (sent in authorize request, echoed in token)
at_hash โ Hash of the access token (binds the ID token to the access token)
amr โ Authentication methods used
Access Token (JWT)
{
"iss": "https://identity.example.com",
"aud": "https://api.example.com",
"client_id": "web.app",
"sub": "818727",
"scope": "openid profile api1",
"exp": 1311284570,
"iat": 1311280970,
"jti": "unique-token-id"
}
Key claims:
aud โ The API resource(s) this token is valid for
client_id โ Which client requested this token
scope โ Granted permissions
jti โ Unique token identifier (for revocation tracking)
Reference Tokens
Reference tokens are not self-contained JWTs. Instead, the access token is an opaque identifier. The API must call the introspection endpoint to validate it:
POST /connect/introspect
Content-Type: application/x-www-form-urlencoded
token=<reference_token>&token_type_hint=access_token
When to use reference tokens:
- Tokens contain sensitive claims you don't want exposed to intermediaries
- You need immediate token revocation (JWT lifetimes are not revocable until expiry)
- Token size is a concern (reference tokens are small opaque strings)
Concept 5: Token Introspection and Revocation
Introspection
APIs validate reference tokens by calling the introspection endpoint. The API authenticates itself with its own secret:
var introspectionResponse = await httpClient.IntrospectTokenAsync(
new TokenIntrospectionRequest
{
Address = disco.IntrospectionEndpoint,
ClientId = "api1",
ClientSecret = "api1-secret",
Token = accessToken
});
if (!introspectionResponse.IsActive)
{
}
Revocation
Clients can revoke access tokens and refresh tokens:
var revocationResponse = await httpClient.RevokeTokenAsync(
new TokenRevocationRequest
{
Address = disco.RevocationEndpoint,
ClientId = "web.app",
ClientSecret = "secret",
Token = refreshToken,
TokenTypeHint = "refresh_token"
});
Concept 6: Scopes and Claims Mapping
Scopes Control What's in Tokens
| Scope requested | What it controls | Token affected |
|---|
openid | Returns sub claim | ID token |
profile | Returns name, family_name, etc. | ID token / userinfo |
email | Returns email, email_verified | ID token / userinfo |
api1 | Grants access to API | Access token |
offline_access | Returns refresh token | Refresh token issued |
Claims Destinations
By default, IdentityServer emits identity claims to the ID token and the userinfo endpoint. Claims associated with API scopes go into the access token. The IProfileService controls claim emission:
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var claims = GetClaimsForUser(context.Subject);
context.IssuedClaims.AddRange(
claims.Where(c => context.RequestedClaimTypes.Contains(c.Type)));
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
Concept 7: Security Extensions
Pushed Authorization Requests (PAR)
PAR moves the authorization parameters from the query string to a backchannel POST, preventing parameter tampering and URL length issues:
1. Client POSTs parameters to /connect/par โ gets a request_uri
2. Client redirects user to /authorize?request_uri=...&client_id=...
DPoP (Demonstrating Proof-of-Possession)
DPoP binds access tokens to a client's cryptographic key, preventing token theft and replay:
1. Client generates a key pair
2. Client creates a DPoP proof (signed JWT with the public key)
3. Client sends the DPoP proof in the DPoP header with the token request
4. IdentityServer binds the token to the key via a "cnf" claim
5. API verifies the DPoP proof matches the token's "cnf" claim
FAPI 2.0
Financial-grade API profile requires PAR, DPoP or mTLS, and stricter validation. Duende IdentityServer supports FAPI 2.0 compliance from v7.3+.
Common Pitfalls
1. Using Access Tokens for Authentication
var userId = accessToken.Claims.First(c => c.Type == "sub").Value;
var userId = User.FindFirst("sub")?.Value;
2. Parsing Access Tokens in the Client
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(accessToken);
httpClient.SetBearerToken(accessToken);
3. Missing PKCE
4. Ignoring Token Expiration
httpClient.SetBearerToken(cachedAccessToken);
builder.Services.AddOpenIdConnectAccessTokenManagement();
5. Hardcoding Endpoint URLs
var tokenEndpoint = "https://identity.example.com/connect/token";
var disco = await httpClient.GetDiscoveryDocumentAsync(authority);
var tokenEndpoint = disco.TokenEndpoint;
Protocol Debugging Checklist
When a token exchange fails, check these in order:
- Discovery document โ Is
/.well-known/openid-configuration reachable? Does it return valid JSON?
- Client ID โ Does the client ID in the request exactly match the server registration?
- Redirect URI โ Exact string match including scheme, host, port, path, and trailing slash
- Scopes โ Are all requested scopes registered in
AllowedScopes on the client?
- Grant type โ Is the grant type in the request allowed by the client's
AllowedGrantTypes?
- PKCE โ Is the client sending
code_challenge and code_verifier? Duende IS requires PKCE by default.
- Client secret โ Is the secret correct? Check for encoding issues (Sha256 hash, not plaintext).
- Clock skew โ Is the server time within acceptable bounds for token validation? (default: 5 min)
- HTTPS โ Is the authorize redirect using HTTPS? Mixed content blocks cause silent failures.
- CORS โ If calling the token endpoint from a browser, is the origin in
AllowedCorsOrigins?
Resources