OAuth2 and OpenID Connect: Authentication and Authorization Standards
A practical guide to OAuth2 and OpenID Connect — the standards underpinning secure login and identity management across the modern web — covering the core distinction between authentication and authorization, grant types, tokens, PKCE, and implementation in ASP.NET Core.
Table of Contents
- Introduction
- Authentication vs. Authorization: The Distinction That Matters
- OAuth2 Core Concepts
- Grant Types (Flows)
- PKCE: Securing the Authorization Code Flow
- OpenID Connect: Adding Identity on Top of OAuth2
- Tokens in Depth
- Scopes and Claims
- Implementing OIDC Login in ASP.NET Core
- Securing an API with OAuth2
- Refresh Tokens and Token Lifecycle
- Common Identity Providers
- Common Vulnerabilities and Pitfalls
- Quick Reference Table
- Conclusion
Introduction
OAuth2 and OpenID Connect (OIDC) are the two standards behind nearly every "Sign in with Google/Microsoft/GitHub" button and behind how modern APIs verify who's calling them and what they're allowed to do. OAuth2 is fundamentally an authorization framework — it lets an application access resources on a user's behalf without ever seeing that user's password. OpenID Connect is a thin, standardized authentication layer built directly on top of OAuth2, adding a well-defined way to actually verify who the user is.
User → clicks "Sign in with Microsoft" →
redirected to Microsoft's login page →
authenticates directly with Microsoft (app never sees the password) →
redirected back to the app with proof of identity and/or an access token
That flow — delegating authentication to a trusted identity provider rather than an application managing its own passwords — is the practical outcome these two standards make possible, safely and interoperably, across every major platform and language.
1. Authentication vs. Authorization: The Distinction That Matters
This is the single most important conceptual distinction in this entire space, and confusing the two is the root of a large share of real-world security mistakes.
Authentication: "Who are you?"
Authentication answers the question of identity — verifying that a user actually is who they claim to be, typically by them proving they know a password, control an email account, or possess a registered device (multi-factor authentication).
Authorization: "What are you allowed to do?"
Authorization answers a separate question — given that we know who you are (or even without necessarily knowing exactly who you are), what actions and resources are you permitted to access.
Why OAuth2 alone doesn't answer "who are you?"
OAuth2 access token: proves "this app has permission to call the Photos API on behalf of some user"
— it does NOT reliably prove who that user actually is, in a standardized way
OAuth2 was originally designed purely for delegated authorization — letting a third-party app (say, a printing service) access your photos on a cloud storage provider without you handing over your cloud storage password. It was never designed to answer "who is this user" in a standardized, verifiable way — which is exactly the gap that led to widespread, insecure, ad-hoc attempts to (mis)use OAuth2 access tokens as a substitute for authentication, before OpenID Connect formalized the correct way to do this (Section 5).
The rule of thumb
If your application needs to know who a user is (to show their name, personalize their experience, make identity-based decisions) — that's authentication, and OpenID Connect is the right tool. If your application needs to access a resource on a user's behalf, or an API needs to decide whether a caller has permission to perform an action — that's authorization, and OAuth2 alone is the right tool. Most real applications need both, which is exactly why OIDC builds on top of OAuth2 rather than replacing it.
2. OAuth2 Core Concepts
The four roles
Resource Owner — the user who owns the data/account (e.g., "Ada")
Client — the application requesting access (e.g., "PhotoPrinter App")
Authorization Server — issues tokens after authenticating the user and obtaining consent (e.g., "Google's login system")
Resource Server — the API that actually holds the protected data (e.g., "Google Photos API")
Understanding these four distinct roles is foundational to reading any OAuth2 diagram or specification — a common point of confusion is conflating the Authorization Server and Resource Server, which are often operated by the same company but are conceptually and sometimes physically distinct systems (an identity provider like Microsoft Entra ID as the Authorization Server, versus your own company's API as the Resource Server).
The basic delegation flow
1. Client redirects the Resource Owner to the Authorization Server
2. Resource Owner authenticates and consents to the requested access
3. Authorization Server redirects back to the Client with an authorization code
4. Client exchanges the code for an access token (and, in OIDC, an ID token)
5. Client uses the access token to call the Resource Server on the Resource Owner's behalf
This is the shape of the Authorization Code flow specifically (Section 3) — the most common and most secure grant type for applications with a backend server component.
Client types
Confidential client — can securely store a secret (a traditional server-side web app; the secret never leaves the server)
Public client — cannot securely store a secret (a mobile app, or a JavaScript SPA running entirely in the browser)
This distinction directly determines which grant type and which additional protections (PKCE, Section 4) are appropriate — a public client that can't keep a secret confidential needs a fundamentally different security approach than a confidential client that can.
3. Grant Types (Flows)
Authorization Code flow (the standard, recommended flow)
GET https://authserver.com/authorize?
response_type=code&
client_id=abc123&
redirect_uri=https://myapp.com/callback&
scope=openid profile email&
state=xyz789
POST https://authserver.com/token
grant_type=authorization_code
code=<the code received above>
client_id=abc123
client_secret=<confidential client secret>
redirect_uri=https://myapp.com/callback
The user is redirected to the Authorization Server, authenticates there directly, and is redirected back with a short-lived authorization code — which the application's backend then exchanges (using its confidential client secret) for the actual tokens. Critically, the authorization code itself is useless without the client secret to exchange it, and it's transmitted via a redirect (visible in browser history/logs) rather than the tokens themselves being exposed that way — this is why it remains the recommended flow for any application with a secure backend component.
Authorization Code flow with PKCE (for public clients)
Covered in depth in Section 4 — the same fundamental flow, adapted for clients (mobile apps, SPAs) that can't hold a confidential secret.
Client Credentials flow (machine-to-machine, no user involved)
POST https://authserver.com/token
grant_type=client_credentials
client_id=service-a
client_secret=<secret>
scope=api://service-b/.default
Used when there's no user in the picture at all — one backend service authenticating directly as itself to call another service or API, common for the microservice-to-microservice patterns covered in this series' gRPC and Background Services guides. The resulting access token represents "this specific service," not any particular user.
Resource Owner Password Credentials (ROPC) — largely deprecated, avoid
POST https://authserver.com/token
grant_type=password
username=ada@example.com
password=<the user's actual password>
client_id=abc123
This flow has the client application collect the user's actual password directly and exchange it for a token — defeating the entire point of OAuth2 (never letting a third-party application see the user's credentials). It's explicitly discouraged in modern OAuth2 guidance (deprecated outright in OAuth 2.1) and should only ever be considered for a first-party application with no reasonable alternative, essentially never for anything involving a third-party identity provider.
Implicit flow — deprecated, do not use for new applications
GET https://authserver.com/authorize?
response_type=token&
client_id=abc123&
redirect_uri=https://myapp.com/callback
Historically used by pure JavaScript SPAs (since they were public clients unable to securely exchange an authorization code), the Implicit flow returned the access token directly in the URL fragment after redirect — a design now considered insecure (tokens end up in browser history, referrer headers, and are vulnerable to interception) and formally deprecated in OAuth 2.1. Authorization Code flow with PKCE has fully replaced it as the recommended approach even for public clients like SPAs.
Device Authorization flow (for input-constrained devices)
POST https://authserver.com/device_authorization
client_id=abc123
Please visit https://authserver.com/activate and enter code: WDJB-MJHT
Used for devices without a convenient way to type credentials or navigate a full browser flow — a smart TV, a CLI tool — where the user completes authentication on a separate device (typically their phone), and the constrained device polls the Authorization Server until that separate authentication completes.
4. PKCE: Securing the Authorization Code Flow
The vulnerability PKCE addresses
A public client (a mobile app, a SPA) can't hold a confidential client secret — anyone could extract it from the app's binary or browser bundle. Without additional protection, this means an attacker who intercepts the authorization code (via a malicious app registered to the same custom URL scheme on a mobile device, for instance) could exchange it for tokens themselves, since the code exchange step would otherwise require no additional secret at all for a public client.
How PKCE (Proof Key for Code Exchange) fixes this
1. Client generates a random "code_verifier" (kept secret, held only in the client)
2. Client computes code_challenge = SHA256(code_verifier), sent in the initial authorize request
GET https://authserver.com/authorize?
response_type=code&
client_id=abc123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
3. Authorization Server stores the code_challenge alongside the issued authorization code
4. Client exchanges the code, now also sending the original plaintext code_verifier
POST https://authserver.com/token
grant_type=authorization_code
code=<the code>
code_verifier=<the original random secret from step 1>
5. Authorization Server hashes the received code_verifier and confirms it matches the stored code_challenge
Even if an attacker intercepts the authorization code in transit, they can't complete the token exchange without also possessing the original code_verifier — which never left the legitimate client that generated it. PKCE effectively gives a public client a dynamically generated, single-use secret for each individual authorization attempt, solving the "public clients can't hold a static secret" problem without needing one.
PKCE is now recommended for confidential clients too
While PKCE was originally designed specifically for public clients, modern OAuth2 guidance (and OAuth 2.1) recommends using it universally, even for confidential server-side clients — it's a genuinely low-cost, defense-in-depth addition against authorization code interception attacks regardless of client type, so there's little reason not to include it even where it's not strictly required.
5. OpenID Connect: Adding Identity on Top of OAuth2
The ID token: OIDC's core addition
GET https://authserver.com/authorize?
response_type=code&
client_id=abc123&
scope=openid profile email& ← the "openid" scope triggers OIDC behavior
redirect_uri=https://myapp.com/callback
Requesting the openid scope alongside the standard OAuth2 flow triggers the Authorization Server to also issue an ID token — a signed JWT (Section 6) containing standardized claims about the authenticated user's identity, which is the piece OAuth2 alone never defined.
// Decoded ID token payload
{
"iss": "https://authserver.com",
"sub": "248289761001",
"aud": "abc123",
"exp": 1721404800,
"iat": 1721401200,
"name": "Ada Lovelace",
"email": "ada@example.com",
"email_verified": true
}
-
iss(issuer) — which Authorization Server issued this token, letting the client verify it came from a trusted source. -
sub(subject) — a stable, unique identifier for this specific user, the correct value to use as your application's internal user identifier (not the email, which can change). -
aud(audience) — which client this token was issued for, preventing a token issued for one application from being replayed against a different one. -
exp/iat— expiration and issued-at timestamps.
Discovery: standardized metadata
GET https://authserver.com/.well-known/openid-configuration
{
"issuer": "https://authserver.com",
"authorization_endpoint": "https://authserver.com/authorize",
"token_endpoint": "https://authserver.com/token",
"jwks_uri": "https://authserver.com/.well-known/jwks.json",
"userinfo_endpoint": "https://authserver.com/userinfo"
}
Every OIDC-compliant provider exposes this standardized discovery document, letting a client library automatically fetch all the endpoint URLs and signing keys it needs, rather than every application hardcoding provider-specific configuration — this is a large part of why swapping identity providers (or supporting several) is comparatively straightforward in a properly OIDC-compliant client library.
The UserInfo endpoint
GET https://authserver.com/userinfo
Authorization: Bearer <access_token>
{ "sub": "248289761001", "name": "Ada Lovelace", "email": "ada@example.com" }
An additional endpoint for fetching (or re-fetching) the current user's profile information using the access token — useful when you need fresher profile data than what was captured in the ID token at login time, without requiring the user to re-authenticate.
6. Tokens in Depth
JWTs: the common (but not mandatory) token format
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
^ header ^ payload (claims) ^ signature
A JSON Web Token (JWT) encodes a header, a set of claims, and a cryptographic signature, all base64url-encoded and dot-separated — the signature lets a resource server verify the token's authenticity and integrity without a network call back to the Authorization Server, since it can validate the signature locally using the issuer's published public key (from the jwks_uri discovery endpoint). This is a significant part of JWTs' popularity for access tokens: verification is fast and doesn't create a dependency on the Authorization Server's availability for every single API request.
Access tokens vs. ID tokens: don't confuse their purposes
ID token: "Here is proof of who authenticated, intended for the CLIENT APPLICATION to consume"
Access token: "Here is proof this request is authorized, intended for the RESOURCE SERVER/API to consume"
A genuinely common and consequential mistake: using an ID token to authenticate API calls (it wasn't designed for that — its aud claim identifies your client application, not your API, and resource servers have no standardized obligation to accept it), or trying to extract user identity claims from an access token (in the OAuth2 spec, an access token's format and content are technically opaque to the client — some providers happen to issue JWT access tokens with useful claims, but this isn't a guarantee you should design around). Keep the two conceptually and practically separate: ID tokens are for the client to learn who logged in; access tokens are for calling protected APIs.
Opaque tokens vs. JWTs
Opaque: 4f8a92c1-7b3e-4d5a-9f1c-2e8b6a4d7c9e (meaningless without calling back to the Authorization Server)
JWT: eyJhbGciOiJSUzI1NiIs... (self-contained, locally verifiable)
Not every access token is a JWT — an opaque token is just a random reference string; the resource server must call back to the Authorization Server's introspection endpoint to validate it and learn anything about it. Opaque tokens allow instant, server-side revocation (the Authorization Server simply stops recognizing the token) at the cost of a network round-trip per validation; JWTs validate locally and fast, but genuinely instant revocation before natural expiry is harder to achieve (typically requiring a token blocklist check anyway, partially undermining the "no network call needed" benefit for that specific case).
7. Scopes and Claims
Scopes: what the client is asking permission for
scope=openid profile email api://products/read api://orders/write
Scopes are space-separated strings the client requests during authorization, representing the specific permissions being asked for — openid (triggers OIDC/ID token issuance), profile/email (standard OIDC scopes requesting specific claim categories), and custom, API-specific scopes (api://products/read) defined by whoever owns that particular resource server.
The consent screen: scopes made visible to the user
"PhotoPrinter App" wants to:
✓ View your basic profile information
✓ View your email address
✓ Access and print your photos
The scopes requested directly drive what a user sees on the consent screen — requesting only the minimum scopes actually needed (rather than broadly requesting everything that might conceivably be useful someday) is both a security best practice (least privilege) and a genuine user-trust consideration, since an application requesting conspicuously broad permissions for a narrow stated purpose is a well-known red flag users (and increasingly, platform review processes) have learned to be wary of.
Claims: the actual information delivered
{ "sub": "123", "name": "Ada Lovelace", "email": "ada@example.com", "roles": ["admin", "editor"] }
Claims are the individual pieces of information actually delivered in a token — some are standardized by the OIDC specification (sub, name, email, email_verified), and identity providers commonly support custom claims for application- or organization-specific data (roles, tenant IDs, entitlements) configured per client registration.
8. Implementing OIDC Login in ASP.NET Core
Basic setup with a generic OIDC provider
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.ClientId = "abc123";
options.ClientSecret = builder.Configuration["Authentication:ClientSecret"];
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("email");
});
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/login", () => Results.Challenge(new AuthenticationProperties { RedirectUri = "/" },
new[] { OpenIdConnectDefaults.AuthenticationScheme }));
This combination — a cookie scheme for maintaining the local session after login, and an OpenID Connect scheme for the actual authentication handshake with the Authorization Server — is the standard pattern for a traditional server-rendered ASP.NET Core application: after a successful OIDC login, ASP.NET Core issues its own local session cookie, and subsequent requests are authenticated via that cookie rather than repeating the OIDC flow on every request.
Microsoft Entra ID via Microsoft.Identity.Web
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret"
}
}
Microsoft.Identity.Web wraps the generic OIDC handler with Entra ID-specific conveniences (token caching, incremental consent, easier downstream API token acquisition) — the recommended library specifically for applications authenticating against Microsoft Entra ID rather than configuring the generic OIDC handler by hand.
Accessing user claims after login
app.MapGet("/profile", (ClaimsPrincipal user) =>
{
var name = user.FindFirstValue(ClaimTypes.Name);
var email = user.FindFirstValue(ClaimTypes.Email);
return Results.Ok(new { name, email });
}).RequireAuthorization();
Once authenticated, the claims from the ID token (and any additional claims configured by the identity provider) are available on HttpContext.User as a standard ClaimsPrincipal, the same underlying ASP.NET Core identity model used for authorization policies covered in this series' ASP.NET Core guide.
9. Securing an API with OAuth2
Validating JWT access tokens
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = "api://my-api";
});
builder.Services.AddAuthorization();
app.MapGet("/products", () => GetProducts())
.RequireAuthorization();
An API secured this way validates every incoming request's Authorization: Bearer <token> header — checking the JWT's signature (against the Authority's published public keys, fetched automatically via the discovery document from Section 5), its aud claim (matching the configured Audience), and its expiry, all without the API needing to call back to the Authorization Server on every request (as covered in Section 6's JWT verification discussion).
Scope-based authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanReadProducts", policy => policy.RequireClaim("scp", "products.read"));
options.AddPolicy("CanWriteProducts", policy => policy.RequireClaim("scp", "products.write"));
});
app.MapGet("/products", () => GetProducts()).RequireAuthorization("CanReadProducts");
app.MapPost("/products", (Product p) => CreateProduct(p)).RequireAuthorization("CanWriteProducts");
Mapping OAuth2 scopes present in the validated access token to ASP.NET Core authorization policies (as covered more generally in this series' ASP.NET Core guide) is the standard way to enforce fine-grained, per-endpoint permission checks based on what the calling client was actually granted, rather than a single all-or-nothing "authenticated or not" check.
10. Refresh Tokens and Token Lifecycle
Why access tokens are short-lived
Access token lifetime: typically 15 minutes to a couple of hours
Deliberately short access token lifetimes limit the damage window if a token is ever leaked or intercepted — an attacker with a stolen access token only has a narrow window to misuse it before it naturally expires, rather than indefinite access.
Refresh tokens: obtaining new access tokens without re-authenticating
POST https://authserver.com/token
grant_type=refresh_token
refresh_token=<the refresh token>
client_id=abc123
client_secret=<confidential client secret>
A refresh token (typically longer-lived, sometimes lasting weeks or until explicitly revoked) lets a client obtain a fresh access token without forcing the user to log in again — the client silently exchanges the refresh token for a new access token whenever the current one is close to expiring, keeping the user's session effectively continuous without repeatedly interrupting them for re-authentication.
Refresh token rotation
Each refresh_token grant issues a NEW refresh token, and the old one is immediately invalidated
Refresh token rotation — issuing a brand-new refresh token on every use and invalidating the previous one — is a security best practice that limits the impact of a leaked refresh token: if a stolen refresh token is used by an attacker, and rotation is enabled, the legitimate client's next attempt to use its (now-invalidated) refresh token will fail conspicuously, giving the system a detectable signal that token theft may have occurred, rather than both the attacker and the legitimate client silently continuing to use the same long-lived token indefinitely.
Storing tokens securely
options.SaveTokens = true; // ASP.NET Core stores tokens in the encrypted authentication cookie/session, not client-side JS-accessible storage
For server-rendered applications, tokens should be kept server-side (in an encrypted cookie or server session, never exposed to client-side JavaScript). For SPAs, storing tokens in localStorage is a well-known XSS risk (any injected script can read them) — an HttpOnly cookie-based session (with the actual OIDC/OAuth2 exchange happening via a backend-for-frontend pattern) is the more robustly secure approach, at the cost of additional backend complexity compared to a pure client-side SPA handling tokens directly.
11. Common Identity Providers
| Provider | Notes |
|---|---|
| Microsoft Entra ID (formerly Azure AD) | The standard choice for organizations in the Microsoft/Azure ecosystem; deep integration with Microsoft.Identity.Web
|
| Auth0 | Popular third-party Identity-as-a-Service platform, broad social/enterprise identity provider support |
| Okta | Enterprise-focused identity platform, strong SSO/enterprise directory integration |
| Google Identity Platform | For "Sign in with Google" and Google Workspace-integrated scenarios |
| Keycloak | Open-source, self-hostable identity provider — a common choice when data residency or avoiding a third-party dependency matters |
| AWS Cognito | AWS-native identity service, commonly paired with the AWS compute services covered elsewhere in this series |
Build vs. buy
Implementing a fully compliant, secure OAuth2/OIDC Authorization Server from scratch is a substantial, security-critical undertaking that's generally not worth attempting for most organizations — nearly every real-world scenario is better served by using one of the established identity providers above (or a self-hosted option like Keycloak if avoiding a third-party SaaS dependency matters) rather than building custom authentication logic, given how much subtle, security-critical detail these standards involve.
12. Common Vulnerabilities and Pitfalls
| Pitfall | Why it hurts | Better approach |
|---|---|---|
| Using the Implicit flow for new applications | Deprecated, exposes tokens in the URL fragment/browser history | Use Authorization Code flow with PKCE, even for SPAs |
Storing tokens in localStorage for a SPA |
Readable by any injected script (XSS) | Prefer an HttpOnly cookie-based session via a backend-for-frontend |
| Using an ID token to call an API | ID tokens aren't intended for resource servers; aud doesn't match the API |
Use access tokens for API calls; ID tokens are for the client application only |
Skipping state parameter validation |
Vulnerable to CSRF attacks against the OAuth2 redirect flow | Always generate, send, and validate the state parameter |
| Requesting overly broad scopes | Increases blast radius if a token leaks; erodes user trust at consent | Request only the minimum scopes actually needed |
Not validating the aud/iss claims on an incoming access token |
A token issued for a different application/API could be accepted incorrectly | Always validate audience and issuer, not just the signature |
| Resource Owner Password Credentials flow with a third-party IdP | Defeats the entire purpose of OAuth2 — the app sees the actual password | Avoid ROPC entirely for anything beyond a narrow first-party legacy scenario |
| No refresh token rotation | A leaked refresh token grants indefinite access with no detection signal | Enable rotation so reuse of an invalidated token is detectable |
Quick Reference Table
| Concept | Purpose |
|---|---|
| OAuth2 | Delegated authorization framework — "can this app access this resource" |
| OpenID Connect | Standardized authentication layer built on OAuth2 — "who is this user" |
| Authorization Code flow | The standard, recommended grant type for apps with a backend |
| PKCE | Secures the Authorization Code flow for public clients (SPAs, mobile apps) |
| Client Credentials flow | Machine-to-machine authentication with no user involved |
| ID token | JWT proving user identity, intended for the client application |
| Access token | Credential presented to a resource server/API, may be opaque or a JWT |
| Refresh token | Long-lived credential for obtaining new access tokens silently |
| Scopes | Requested permissions, visible to the user at consent |
| Claims | The actual identity/permission data delivered in a token |
| Discovery document | Standardized .well-known/openid-configuration metadata endpoint |
Conclusion
OAuth2 and OpenID Connect solve two related but genuinely distinct problems — delegated authorization and standardized authentication — and a great deal of real-world security trouble in this space traces back to conflating the two, or reaching for a deprecated flow (Implicit, ROPC) out of habit or outdated tutorials rather than the modern, PKCE-protected Authorization Code flow that current guidance consistently recommends for essentially every client type.
The practical path for the overwhelming majority of applications is straightforward: use an established identity provider rather than building authentication infrastructure from scratch, use the Authorization Code flow with PKCE regardless of whether your client is confidential or public, keep ID tokens and access tokens in their intended, separate lanes, and validate every claim (aud, iss, signature, expiry) an API relies on rather than trusting a token just because it happened to arrive in an Authorization header. Get those fundamentals right, and the substantial cryptographic and protocol complexity underneath OAuth2/OIDC stays exactly where it should be — handled by well-vetted libraries and identity providers, not something your application code needs to reason about directly.
Found this useful? Feel free to star the repo, open an issue with corrections, or share the ID-token-vs-access-token mix-up that taught you to read the spec more carefully.
Top comments (0)