In modern web and mobile applications, social logins like “Login with Google” or “Login with GitHub” have become commonplace. Behind these are two standard specifications: OAuth 2.0 and OpenID Connect (OIDC). This article explains these mechanisms from basics to advanced applications.
Differences Between OAuth 2.0 and OIDC
First, let’s clarify the often-confused differences between OAuth 2.0 and OIDC.
| Item | OAuth 2.0 | OpenID Connect |
|---|---|---|
| Purpose | Authorization | Authentication |
| Question answered | ”Will you allow this app access to resources?" | "Who are you?” |
| What you get | Access token | ID token + Access token |
| Year established | 2012 (RFC 6749) | 2014 |
Difference between Authentication and Authorization:
- Authentication: Process of confirming “who the user is”
- Authorization: Process of determining “what the user can access”
OAuth 2.0 Actors
OAuth 2.0 has four main roles.
flowchart TB
RO["Resource Owner (User)<br/>Owner of the data"]
AS["Authorization Server<br/>Issues tokens (Google, GitHub, etc.)"]
Client["Client<br/>Application that wants to access resources"]
RS["Resource Server<br/>Hosts protected resources (APIs, etc.)"]
RO -->|"Grants authorization"| AS
AS -->|"Issues token"| Client
Client -->|"API request"| RS
OAuth 2.0 Grant Types
OAuth 2.0 defines multiple “grant types” (flows) for different use cases.
1. Authorization Code Grant
The most secure and recommended flow. Used in server-side applications.
sequenceDiagram
participant U as User
participant C as Client
participant A as Auth Server
participant R as Resource Server
U->>C: 1. Start login
C->>A: 2. Auth request
A->>U: 3. Login screen / consent
U->>A: 4. Authenticate/consent
A->>C: 5. Auth code
C->>A: 6. Token exchange
A->>C: 7. Access token
C->>R: 8. API call
R->>C: 9. Resource
// Step 2: Generate authorization request URL
const authorizationUrl = new URL('https://auth.example.com/authorize');
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', CLIENT_ID);
authorizationUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authorizationUrl.searchParams.set('scope', 'openid profile email');
authorizationUrl.searchParams.set('state', generateRandomState());
// Step 6: Token exchange
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
2. Authorization Code Grant with PKCE
An extension for environments like SPAs and mobile apps where client secrets cannot be stored securely.
// PKCE: Proof Key for Code Exchange
import crypto from 'crypto';
// Step 1: Generate Code Verifier (43-128 random characters)
const codeVerifier = crypto.randomBytes(32)
.toString('base64url');
// Step 2: Generate Code Challenge
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// Step 3: Add code_challenge to authorization request
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateState());
// Step 4: Send code_verifier during token exchange
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier, // Send verifier instead of secret
}),
});
3. Client Credentials Grant
Used for server-to-server communication (M2M: Machine to Machine). No user involvement.
// Backend service authentication
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'api:read api:write',
}),
});
const { access_token } = await tokenResponse.json();
4. Refresh Token Grant
Flow for renewing access tokens.
// Renewing access token with refresh token
const refreshResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: CLIENT_ID,
}),
});
const { access_token, refresh_token } = await refreshResponse.json();
// Update storage if new refresh token is returned
OpenID Connect (OIDC)
OIDC is an authentication layer built on top of OAuth 2.0.
ID Token Structure
In OIDC, an ID token (JWT format) is issued in addition to the access token.
| Section | Content |
|---|---|
| Header | {"alg": "RS256", "typ": "JWT", "kid": "key-id-123"} |
| Payload | iss (Issuer), sub (Subject), aud (Audience), exp (Expiration), iat (Issued at), nonce (Replay prevention), email, name |
| Signature | Signed with authorization server’s private key |
ID Token Validation
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
// JWKS (JSON Web Key Set) client configuration
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
rateLimit: true,
});
// Function to get public key
const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
};
// Verify ID token
const verifyIdToken = (idToken: string): Promise<jwt.JwtPayload> => {
return new Promise((resolve, reject) => {
jwt.verify(
idToken,
getKey,
{
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: CLIENT_ID,
},
(err, decoded) => {
if (err) reject(err);
else resolve(decoded as jwt.JwtPayload);
}
);
});
};
Scope Design
OIDC Standard Scopes
| Scope | Returned Claims |
|---|---|
openid | sub (required) |
profile | name, family_name, given_name, picture, etc. |
email | email, email_verified |
address | address |
phone | phone_number, phone_number_verified |
Custom Scope Example
// Custom scopes for API access
const scopes = [
'openid', // OIDC required
'profile', // Basic profile
'email', // Email address
'api:read', // API read permission
'api:write', // API write permission
'admin:users', // User management permission
];
Security Best Practices
1. CSRF Protection with State Parameter
// Generate unique state for each session
const generateState = (): string => {
const state = crypto.randomBytes(32).toString('hex');
// Save to session
session.oauthState = state;
return state;
};
// Validate state on callback
const validateState = (receivedState: string): boolean => {
const isValid = receivedState === session.oauthState;
delete session.oauthState; // Delete after use
return isValid;
};
2. Replay Attack Prevention with Nonce
// Include nonce in ID token request
const nonce = crypto.randomBytes(16).toString('hex');
session.oidcNonce = nonce;
authUrl.searchParams.set('nonce', nonce);
// Verify nonce when validating ID token
const decoded = await verifyIdToken(idToken);
if (decoded.nonce !== session.oidcNonce) {
throw new Error('Invalid nonce');
}
3. Secure Token Storage
// Backend: Session management with HTTPOnly Cookie
res.cookie('session_id', sessionId, {
httpOnly: true, // Not accessible from JavaScript
secure: true, // HTTPS required
sameSite: 'lax', // CSRF protection
maxAge: 3600000, // 1 hour
});
// Frontend (SPA): Keep in memory
// Avoid LocalStorage as it's vulnerable to XSS
class TokenStore {
private accessToken: string | null = null;
setToken(token: string) {
this.accessToken = token;
}
getToken() {
return this.accessToken;
}
}
4. Token Expiration Design
| Token Type | Recommended Expiration |
|---|---|
| Access token | 15 min - 1 hour |
| Refresh token | 7 - 30 days (absolute) |
| ID token | 5 min - 1 hour |
| Authorization code | Within 10 minutes |
Summary
OAuth 2.0 and OIDC are foundational technologies for modern web authentication and authorization.
OAuth 2.0 Key Points
- Purpose: Authorization (delegation of resource access rights)
- Recommended flow: Authorization Code Grant + PKCE
- Tokens: Access token, refresh token
OIDC Key Points
- Purpose: Authentication (user identity verification)
- Added element: ID token (JWT format)
- Standard scopes: openid, profile, email, etc.
Security Requirements
- Use PKCE (especially for SPA/mobile)
- CSRF protection with state parameter
- Replay attack prevention with nonce
- Token storage with HTTPOnly Cookie
- Short access token expiration
By correctly understanding and implementing these concepts, you can build secure and user-friendly authentication systems.
Reference Links
- RFC 6749 - OAuth 2.0
- RFC 7636 - PKCE
- OpenID Connect Core 1.0
- OAuth 2.0 Security Best Current Practice