OAuth 2.0 / OpenID Connect
OAuth 2.0 is an authorization framework that allows applications to access resources on a user’s behalf with limited permissions. OpenID Connect (OIDC) builds on OAuth 2.0 and adds authentication—the foundation for modern single sign-on and social login.
OAuth 2.0 enables a fundamental improvement in security: Instead of giving third-party apps your password (which is actually what used to happen!), the user grants the app only the specific permissions it needs—for a limited time, and revocable at any time. OpenID Connect extends OAuth with standardized identity information.
OAuth 2.0 explained in one sentence
Analogy: In a hotel, you don’t give the valet your house key, but rather a valet key that can only start the car. With OAuth, you don’t give the app your password, but rather a token that only allows certain actions.
Without OAuth (historically): > "Please enter your Google password so we can import your contacts" - The app had full access to everything; revocation was only possible by changing the password.
With OAuth 2.0: > "Allow XY app access to: Your contact list (read-only)" - Token with limited scope; Revocation possible at any time in Google settings, without changing the password.
The OAuth 2.0 Components
The 4 Roles
Resource Owner (User):
- Owns the resources (emails, contacts, etc.)
- Grants access permission
Client (Application):
- App that wants to access resources
- Registers with the Authorization Server (receives Client ID + Secret)
Authorization Server (IdP):
- Issues Access Tokens upon consent
- Examples: Google, Microsoft Entra ID, Auth0, Keycloak
Resource Server (API):
- Holds the resources (Gmail API, Calendar API)
- Accepts Access Tokens and returns data
Key Concepts
| Term | Meaning |
|---|---|
| Access Token | Short-lived (15 min–1 hr), for API access |
| Refresh Token | Long-lived (7–30 days), retrieves new access tokens |
| Scope | Which permissions? (read:contacts, write:calendar) |
| State | CSRF protection during authorization requests |
| PKCE | Proof Key for Code Exchange (for public clients) |
OAuth 2.0 Flows
1. Authorization Code Flow + PKCE (recommended for all)
When: Web apps, mobile apps, single-page apps Security: Highest (code is exchanged for a token)
Process:
a) App generates code_verifier (random) + code_challenge (SHA256 of the former)
b) App sends user to IdP:
GET /authorize?
client_id=abc
&response;_type=code
&scope;=openid email
&redirect;_uri=https://app.firma.de/callback
&state;=randomValue
&code;_challenge=<hash>
&code;_challenge_method=S256
c) User logs in to the IdP
d) IdP redirects to callback with authorization code:
GET /callback?code=auth_code_123&state;=randomValue
e) App exchanges code for token:
POST /token
grant_type=authorization_code
&code;=auth_code_123
&redirect;_uri=https://app.firma.de/callback
&client;_id=abc
&code;_verifier=<original_verifier> ← PKCE!
f) IdP returns:
{ "access_token": "...", "refresh_token": "...", "id_token": "..." }
2. Client Credentials Flow
When: Server-to-server (no user involved) Example: Microservice calls another API
POST /token
grant_type=client_credentials
&client;_id=service-a
&client;_secret=secret
&scope;=api:read
Deprecated Flows - Do Not Use
- Implicit Flow (deprecated): Token in URL → History, logs, Referer header; replaced by Authorization Code + PKCE
- Password Grant Flow (deprecated): App receives password directly → OAuth security compromised; only for legacy migration
OpenID Connect (OIDC)
OIDC = OAuth 2.0 + Identity Layer
What OIDC adds:
- ID Token (JWT): Who is the user?
/userinfoEndpoint: Retrieve user information- Standard Scopes:
openid,profile,email,phone,address - Discovery endpoint:
/.well-known/openid-configuration
// ID token content (JWT payload):
{
"iss": "https://accounts.google.com", // Issuer
"sub": "10769150350006150715113082367", // Subject (User ID)
"aud": "client_id_123", // Audience (your app)
"exp": 1311281970, // Expiry
"iat": 1311280970, // Issued At
"nonce": "0394852-3190485-2490358", // CSRF protection
"email": "user@example.com",
"email_verified": true,
"name": "Max Muster"
}
Important validations of the ID token:
iss== known issuer (not "Evil IdP"!)aud== your client ID (no token substitution attack!)exp> now (token not expired)nonce== sent nonce (CSRF protection)- Signature valid with IdP public key
- Algorithm: RS256 or ES256 (not
noneor HS256!)
OAuth Security Vulnerabilities
1. Redirect URI Validation (most common vulnerability)
WRONG: Redirect URI whitelist: "https://app.firma.de/*"
Attack: redirect_uri=https://app.firma.de/callback?next=https://attacker.com
→ OAuth code is forwarded to attacker.com!
CORRECT: Exact URI (no wildcards!):
Whitelist: ["https://app.firma.de/callback"]
2. State parameter missing (CSRF)
Attack: Attacker tricks victim into starting OAuth flow with attacker's code → Victim logs into app using attacker's account. Protection: Set and validate state parameter!
3. Open redirect via Redirect URI
If app redirects to user-specified URL after OAuth:
Attack: ?redirect=/after-login?url=https://phishing.com
Protection: Only allow permitted relative URLs as redirects
4. Authorization Code Interception (without PKCE)
Code visible in URL → Referer header, proxy logs.
Protection: Enforce PKCE (code_challenge)
5. Token Leakage in Logs
Access tokens in URL parameters → Server logs contain tokens. Protection: Tokens ONLY in Authorization headers, never in URLs
6. Consent Screen Phishing (OAuth Consent Phishing)
Attacker registers app with Google/Microsoft using a credible name ("IT Support Tool") and broad scope (offline_access, Mail.ReadWrite). User grants consent → Attacker has full access to mailbox.
Protection: Conditional Access Policy → Admin approval for new apps
Implementation: Node.js with Passport.js
// Express + Passport.js + OIDC
import passport from 'passport';
import { Strategy as OIDCStrategy } from 'passport-openidconnect';
passport.use(new OIDCStrategy({
issuer: 'https://accounts.google.com',
authorizationURL: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenURL: 'https://oauth2.googleapis.com/token',
userInfoURL: 'https://openidconnect.googleapis.com/v1/userinfo',
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'https://app.firma.de/auth/callback',
scope: ['openid', 'email', 'profile'],
// Security:
pkce: true, // Enable PKCE!
state: true, // State parameter for CSRF protection
}, async (issuer, profile, done) => {
// Find or create user in database
const user = await User.findOrCreate({ googleId: profile.id });
return done(null, user);
}));
// Routes:
app.get('/auth/google', passport.authenticate('openidconnect'));
app.get('/auth/callback',
passport.authenticate('openidconnect', { failureRedirect: '/login' }),
(req, res) => res.redirect('/dashboard')
);
```</original_verifier></hash>