OAuth2 and OpenID Connect: A Practical Implementation Guide
Modern applications rarely handle authentication and authorization in isolation. Instead, they rely on battle-tested standards like OAuth2 and OpenID Connect (OIDC). This post breaks down what these protocols do, how they differ, and how to implement them correctly.
OAuth2 vs. OpenID Connect
A common source of confusion is treating these as interchangeable. They are not:
- OAuth2 is an authorization framework. It lets an application obtain limited access to a user's resources without exposing credentials.
- OpenID Connect is an authentication layer built on top of OAuth2. It adds identity by introducing the ID Token.
In short: OAuth2 answers "Can this app access these resources?" while OIDC answers "Who is this user?"
Core Roles
| Role | Description |
|---|---|
| Resource Owner | The user who owns the data |
| Client | The application requesting access |
| Authorization Server | Issues tokens (e.g., Auth0, Keycloak, Okta) |
| Resource Server | The API hosting protected resources |
The Authorization Code Flow with PKCE
For web and mobile apps, the Authorization Code Flow with PKCE (Proof Key for Code Exchange) is the recommended approach. It mitigates authorization code interception attacks.
Step 1: Generate a Code Verifier and Challenge
import crypto from 'crypto';
function base64URLEncode(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
const codeVerifier = base64URLEncode(crypto.randomBytes(32));
const codeChallenge = base64URLEncode(
crypto.createHash('sha256').update(codeVerifier).digest()
);
Step 2: Redirect the User to the Authorization Endpoint
GET https://auth.example.com/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
Note: The
openidscope is what triggers OIDC behavior and requests an ID Token.
Step 3: Exchange the Code for Tokens
After the user authenticates, the authorization server redirects back with a code. Exchange it at the token endpoint:
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: 'https://app.example.com/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: codeVerifier
})
});
const tokens = await response.json();
// { access_token, id_token, refresh_token, expires_in, token_type }
Validating the ID Token
The ID Token is a JWT. Never trust it without validation. At minimum, verify:
- Signature using the provider's public keys (JWKS endpoint).
-
issmatches the expected issuer. -
audmatches your client ID. -
expis in the future. -
noncematches the value you sent (if used).
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://auth.example.com/.well-known/jwks.json')
);
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: 'https://auth.example.com/',
audience: 'YOUR_CLIENT_ID'
});
console.log(`Authenticated user: ${payload.sub}`);
Protecting the Resource Server
The API validates the access token on each request. For opaque tokens, use token introspection; for JWTs, verify locally.
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.slice(7);
jwtVerify(token, JWKS, { issuer, audience })
.then(({ payload }) => { req.user = payload; next(); })
.catch(() => res.status(401).json({ error: 'Invalid token' }));
}
Security Best Practices
- Always use PKCE, even for confidential clients.
-
Validate
stateto prevent CSRF. - Use short-lived access tokens and rotate refresh tokens.
-
Store tokens securely — prefer HTTP-only cookies over
localStoragefor browser apps. - Request minimal scopes following the principle of least privilege.
- Never use the Implicit Flow — it is deprecated in favor of Authorization Code + PKCE.
Conclusion
OAuth2 and OpenID Connect provide a robust foundation for secure authentication and authorization. The key is understanding the separation of concerns: OAuth2 for access, OIDC for identity. By adopting the Authorization Code Flow with PKCE and rigorously validating tokens, you can build systems that are both user-friendly
Top comments (0)