OAuth 2.0 and OIDC: The Difference and When to Use Each
OAuth 2.0 handles authorization while OIDC handles authentication. Learn the grant types, token differences, PKCE, and when to use each protocol.
Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

The "Sign in with Google" Bug That Handed Over Every Account
A startup I consulted for shipped their login flow in a weekend. "Sign in with Google" worked. Users logged in, the app got back an access token from Google's token endpoint, and the backend called https://www.googleapis.com/oauth2/v1/tokeninfo to extract the user's email. Whichever email came back became the logged-in user. Elegant, right? It passed code review. It passed a security scan. It shipped.
Six months later a penetration tester demonstrated full account takeover in 90 seconds. The attack: register any third-party app with Google, trick a victim into granting it the email scope, harvest the resulting access token, and hand that token to the startup's backend. The backend dutifully called Google's tokeninfo endpoint, got back the victim's email, and logged the attacker in as the victim. The access token was never intended for this application -- but the app never checked.
This is "token confusion," the single most common failure mode when teams use OAuth 2.0 for what looks like login. OAuth 2.0 is an authorization framework. It tells you an app is allowed to do something. It does not reliably tell you who the user is. OpenID Connect (OIDC) solves this by issuing ID tokens with a verifiable aud (audience) claim that is bound to your client. If the startup had used OIDC instead of OAuth for login, the attacker's token would have failed the aud check immediately.
This guide is about avoiding that bug -- and the dozen other ways identity flows go sideways. You will click "Sign in with Google" hundreds of times this year. Understanding the two protocols underneath that button, where they overlap and where they diverge, is how you build login flows that do not end up in a breach post-mortem.
OAuth 2.0: Delegated Authorization
OAuth 2.0 is an authorization framework that lets a third-party application obtain limited access to a user's resources on another service, without exposing the user's credentials. It issues access tokens that define what the application can do, not who the user is. That distinction is the one I want burned into your memory -- it is the root cause of the token-confusion bug above.
OAuth 2.0 replaced OAuth 1.0's complicated signature scheme with bearer tokens over TLS. The core idea is simple: instead of giving an app your password, you grant it a token with specific permissions (scopes). The token expires, can be revoked, and limits damage if compromised. The part OAuth deliberately left out of scope was identity -- and that gap is exactly what OIDC was designed to fill.
OAuth 2.0 Grant Types
OAuth 2.0 defines several flows for obtaining tokens. Here's what matters in 2026:
- Authorization Code Grant -- The standard for server-side web apps. The user authenticates with the authorization server, which redirects back with a short-lived code. Your backend exchanges that code for tokens. The user never sees the tokens.
- Authorization Code + PKCE -- Required for single-page apps and mobile apps. PKCE (Proof Key for Code Exchange) adds a code verifier/challenge pair that prevents authorization code interception attacks. This replaced the Implicit flow.
- Client Credentials -- Machine-to-machine communication with no user involved. Your service authenticates directly with the authorization server using a client ID and secret. Used for backend microservice calls.
- Device Authorization Grant -- For devices without browsers (smart TVs, CLI tools). The device displays a code; the user enters it on a separate device with a browser.
Watch out: The Implicit Grant is deprecated. It returned tokens directly in the URL fragment, which exposed them to browser history and referrer headers. If you're still using it, migrate to Authorization Code + PKCE immediately.
Authorization Code Flow Step by Step
- Your app redirects the user to the authorization server's
/authorizeendpoint with the requested scopes. - The user authenticates and consents to the requested permissions.
- The authorization server redirects back to your
redirect_uriwith an authorization code. - Your backend sends the code, client ID, and client secret to the
/tokenendpoint. - The authorization server returns an access token (and optionally a refresh token).
- Your app uses the access token to call the resource server's API.
GET /authorize?
response_type=code
&client_id=your-client-id
&redirect_uri=https://yourapp.com/callback
&scope=read:profile
&state=random-csrf-token
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_FROM_CALLBACK
&redirect_uri=https://yourapp.com/callback
&client_id=your-client-id
&client_secret=your-client-secret
&code_verifier=ORIGINAL_CODE_VERIFIER
What Is OpenID Connect (OIDC)?
Definition: OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. It adds a standardized way to verify user identity by introducing ID tokens (JWTs containing user claims), a userinfo endpoint, and a discovery mechanism. OIDC answers "who is this user?" while OAuth answers "what can this app do?"
Before OIDC, developers abused OAuth 2.0 for authentication by calling a provider-specific API (like Facebook's /me endpoint) with an access token. Every provider did it differently. OIDC standardized this by defining exactly what an identity response looks like.
What OIDC Adds to OAuth 2.0
| Feature | OAuth 2.0 | OIDC |
|---|---|---|
| Purpose | Authorization (access to resources) | Authentication (identity verification) |
| Token type | Access token (opaque or JWT) | ID token (always JWT) + access token |
| User identity | Not standardized | Standard claims (sub, email, name, etc.) |
| Discovery | Not defined | /.well-known/openid-configuration |
| Userinfo endpoint | Provider-specific | Standardized /userinfo |
| Session management | Not defined | Optional spec for session tracking |
The ID Token
The ID token is a JWT signed by the authorization server. It contains standard claims:
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"aud": "your-client-id",
"exp": 1712345678,
"iat": 1712342078,
"nonce": "random-nonce-from-request",
"email": "user@example.com",
"email_verified": true,
"name": "Jane Doe",
"picture": "https://example.com/photo.jpg"
}
Your app validates the ID token by checking the signature against the provider's public keys (published at the JWKS URI), verifying the iss, aud, and exp claims, and confirming the nonce matches what you sent. If all checks pass, you know who the user is.
Standard Scopes and Claims
OIDC defines standard scopes that map to sets of claims:
openid-- Required. Signals this is an OIDC request and triggers ID token issuance.profile-- Name, family name, picture, locale, updated_at.email-- Email address and whether it's verified.address-- Postal address.phone-- Phone number and whether it's verified.
PKCE: Why It Matters for Every Client
PKCE (pronounced "pixie") was originally designed for public clients that can't safely store a client secret -- mobile apps, SPAs, CLI tools. But as of OAuth 2.1, PKCE is recommended for all clients, including confidential ones.
Here's how it works:
- Your app generates a random
code_verifier(43-128 characters). - It computes
code_challenge = BASE64URL(SHA256(code_verifier)). - The code challenge is sent with the authorization request.
- When exchanging the code for tokens, the original code verifier is sent.
- The authorization server hashes the verifier and compares it to the stored challenge.
This prevents an attacker who intercepts the authorization code from exchanging it for tokens -- they don't have the code verifier.
When to Use OAuth, OIDC, or Both
This is the question that trips up most teams. Here's a clear decision framework:
| Scenario | Protocol | Why |
|---|---|---|
| Users log in to your app | OIDC | You need to know who the user is |
| Your app accesses a user's Google Calendar | OAuth 2.0 + OIDC | Identity for login, authorization for API access |
| Microservice-to-microservice calls | OAuth 2.0 (Client Credentials) | No user involved, just service identity |
| Third-party apps access your API | OAuth 2.0 | Delegated access to resources |
| "Sign in with Google/GitHub" button | OIDC | Federated identity verification |
| API gateway token validation | OAuth 2.0 (token introspection or JWT) | Verifying access, not identity |
Pro tip: If you're adding "Sign in with X" to your app, you always want OIDC. Pure OAuth 2.0 tells you the user granted access -- it doesn't reliably tell you who they are. The
openidscope is what triggers the identity layer.
Costs and Provider Comparison
If you're not building your own authorization server, you'll use a managed identity provider. Here's how the major options compare in 2026:
| Provider | Free Tier | Paid Starting At | Best For |
|---|---|---|---|
| Auth0 (Okta) | 25,000 MAU | ~$35/mo | Developer experience, extensive SDKs |
| AWS Cognito | 50,000 MAU | $0.0055/MAU | AWS-native apps, cost efficiency at scale |
| Google Identity Platform | 50,000 MAU | $0.0055/MAU | GCP-native, Firebase integration |
| Keycloak (self-hosted) | Unlimited (OSS) | Infra costs only | Full control, on-prem requirements |
| Clerk | 10,000 MAU | $25/mo | Modern DX, pre-built UI components |
A Correct OIDC Login Handler, Annotated
The token-confusion bug in the opener had a fix that fit in 30 lines. Here is what a production OIDC callback handler should actually do -- every step is a verification that prevents a specific attack class.
import { createRemoteJWKSet, jwtVerify } from 'jose';
const ISSUER = 'https://accounts.google.com';
const CLIENT_ID = process.env.OIDC_CLIENT_ID!;
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/jwks`));
export async function handleCallback(req: Request) {
const url = new URL(req.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
// 1. CSRF protection: verify state matches the one in the session
if (!state || state !== req.session.oauthState) {
throw new Error('state mismatch -- possible CSRF');
}
// 2. Exchange code for tokens (PKCE verifier sent back here)
const tokenRes = await fetch(`${ISSUER}/token`, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code!,
redirect_uri: process.env.OIDC_REDIRECT_URI!,
client_id: CLIENT_ID,
client_secret: process.env.OIDC_CLIENT_SECRET!,
code_verifier: req.session.pkceVerifier,
}),
});
const { id_token } = await tokenRes.json();
// 3. Verify the ID token -- this is where token confusion gets caught
const { payload } = await jwtVerify(id_token, JWKS, {
issuer: ISSUER, // must match the provider
audience: CLIENT_ID, // MUST be our client -- rejects tokens issued to other apps
});
// 4. Verify the nonce matches what we sent in the auth request
if (payload.nonce !== req.session.oidcNonce) {
throw new Error('nonce mismatch -- possible replay');
}
// 5. Only now is it safe to treat payload.sub as the user's identity
return loginUser({ sub: payload.sub as string, email: payload.email as string });
}
Step 3 is the line that would have prevented the opener's breach. jwtVerify enforces aud === CLIENT_ID, so a token minted for any other application is rejected with a verification error. Do not skip it, do not disable audience checking "temporarily for testing," and do not trust an external tokeninfo endpoint to do this for you.
Common Mistakes and How to Avoid Them
Using Access Tokens for Authentication
An access token tells a resource server what the bearer is allowed to do. It doesn't reliably identify the user to your app. Access tokens can be issued to any client, and some providers issue opaque tokens with no user info. Always use the ID token for authentication.
Skipping State Parameter Validation
The state parameter prevents CSRF attacks on the callback endpoint. Generate a random value, store it in the session, and verify it when the callback arrives. Without it, an attacker can craft a URL that logs a victim into the attacker's account.
Not Validating ID Token Claims
You must verify the iss (issuer), aud (audience), exp (expiration), and nonce claims on every ID token. Libraries handle this, but misconfigured audience validation is a frequent source of token confusion attacks.
Frequently Asked Questions
Is OAuth 2.0 an authentication protocol?
No. OAuth 2.0 is an authorization framework. It grants access to resources but doesn't define a standard way to verify user identity. OIDC was built on top of OAuth 2.0 specifically to add authentication. Using OAuth alone for login is possible but requires provider-specific workarounds that OIDC standardizes.
What is the difference between an access token and an ID token?
An access token authorizes API calls to a resource server -- it defines what you can do. An ID token is a JWT that identifies the user -- it defines who you are. Access tokens go to APIs. ID tokens stay with your application. Never send an ID token to an API as a bearer token.
Is the Implicit Grant still safe to use?
No. The Implicit Grant is deprecated in OAuth 2.1. It exposed tokens in browser URLs, making them vulnerable to leakage through browser history, referrer headers, and open redirector attacks. All browser-based apps should use Authorization Code with PKCE instead.
Do I need OIDC if I'm only building machine-to-machine APIs?
No. Machine-to-machine communication uses the OAuth 2.0 Client Credentials grant. There's no user to authenticate, so OIDC adds no value. Your services authenticate with client IDs and secrets (or mutual TLS), and the resulting access tokens authorize API calls.
What is PKCE and when should I use it?
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks by binding the code to the client that requested it. OAuth 2.1 recommends PKCE for all clients -- not just public ones. It adds minimal complexity and closes a real attack vector, so there's no reason to skip it.
Can I use OAuth 2.0 with multiple identity providers simultaneously?
Yes. Your app can support Google, GitHub, and Microsoft as identity providers at the same time. Each provider issues its own tokens. Your app validates each against the correct issuer's keys and maps the sub claim to a local user account. OIDC discovery makes this straightforward to configure.
How do refresh tokens work in OAuth 2.0?
Refresh tokens are long-lived tokens used to obtain new access tokens without user interaction. When an access token expires, your app sends the refresh token to the token endpoint. The server issues a new access token (and optionally rotates the refresh token). Store refresh tokens securely -- they're equivalent to persistent credentials.
Putting It Together
OAuth 2.0 and OIDC aren't competing protocols -- they're complementary layers. OAuth handles authorization, OIDC handles authentication, and most real-world applications need both. Start with OIDC for user login, add OAuth scopes when you need to access third-party APIs, and use Client Credentials for service-to-service calls. Pick a grant type based on your client type, always use PKCE, validate every token properly, and you'll avoid the security pitfalls that catch most teams.
Written by
Abhishek Patel
Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.
Related Articles
Best Vulnerability Scanners for Containers (2026): Snyk vs Trivy vs Grype vs Aqua
Benchmarked comparison of Snyk, Trivy, Grype, and Aqua against 100 production images. Real 2026 pricing, false-positive rates, scan times, and a decision matrix for picking the right scanner.
15 min read
SecurityBest Auth Providers (2026): Auth0 vs Clerk vs Supertokens vs WorkOS vs Supabase Auth
A practitioner comparison of the five dominant auth providers in 2026 -- Auth0, Clerk, Supertokens, WorkOS, and Supabase Auth -- with real pricing tiers, SSO connection math, SOC 2 / HIPAA / FedRAMP coverage, integration code samples, and a decision matrix that maps each vendor to a specific stack and scale.
15 min read
SecurityDPDP Act Compliance Checklist for Indian SaaS Startups (2026): Infrastructure Playbook
A 14-item DPDP Act compliance playbook for Indian SaaS startups in 2026 — data residency on AWS Mumbai, consent capture, DSR workflows, 72-hour breach notifications, and tooling pricing in INR with 18% GST.
20 min read
Enjoyed this article?
Get more like this in your inbox. No spam, unsubscribe anytime.