- Introduction
- Understanding PKCE
- PKCE Flow Overview
- Implementation
- Security Considerations
- Best Practices
- Complete Implementation Example
- References
In modern web applications, securing authentication and authorization processes is paramount. Proof Key for Code Exchange (PKCE) is an extension to the OAuth 2.0 authorization framework that enhances security, especially for public clients unable to securely store client secrets. This document delves into PKCE, elucidates its workflow, and demonstrates its integration with Keycloak using a custom Passport.js strategy written in TypeScript.
Proof Key for Code Exchange (PKCE), pronounced "pixy," is an OAuth 2.0 extension designed to prevent authorization code interception attacks. Initially crafted for mobile and native applications, PKCE is now recommended for all types of OAuth clients, including single-page applications and other public clients.
- Code Verifier: A high-entropy cryptographic random string generated by the client. It serves as a secret used to validate the authorization request.
- Code Challenge: A transformation of the code verifier, typically using SHA-256 hashing followed by Base64 URL encoding. This is sent to the authorization server during the initial authorization request.
- Authorization Code: A temporary code received from the authorization server upon successful user authentication. It is exchanged for access tokens.
- Code Challenge Method: Specifies the method used to derive the code challenge from the code verifier. Commonly,
S256
(SHA-256) is used.
+--------+ +-------------------+ +-----------------+
| | | | | |
| User | | Client App | | Keycloak Auth |
|Browser | | | | Server |
| | | | | |
+---+----+ +---------+---------+ +--------+--------+
| | |
| (A) User initiates login | |
|-------------------------------------------->| |
| | |
| | (B) Generate code_verifier & code_challenge |
| |---------------------------------------------------> |
| | |
| | (C) Redirect to Keycloak with code_challenge |
| |<--------------------------------------------------- |
| | |
| (D) User authenticates & consents | |
|-------------------------------------------->| |
| | |
| | (E) Keycloak redirects to callback with code |
| |---------------------------------------------------> |
| | |
| | |
| | (F) Client receives code & retrieves code_verifier |
| | |
| | (G) Client requests tokens with code & code_verifier|
| |---------------------------------------------------> |
| | |
| | (H) Keycloak validates code_verifier & issues tokens|
| |<--------------------------------------------------- |
| | |
| | (I) Client receives access token |
| | |
| (J) Access protected resources | |
|-------------------------------------------->| |
| | |
+--------+ +---------+---------+ +-----------------+
| | | | | |
|Resource| | | | |
| Server | | | | |
| | | | | |
+--------+ +-------------------+ +-----------------+
Legend:
- (A): User initiates the login process on the Client Application.
- (B): Client generates a
code_verifier
and derives acode_challenge
using SHA-256. - (C): Client redirects the user's browser to Keycloak's authorization endpoint, including the
code_challenge
and specifying thecode_challenge_method
asS256
. - (D): User authenticates with Keycloak and consents to the requested scopes.
- (E): Upon successful authentication, Keycloak redirects the browser back to the Client's callback URL with an authorization code.
- (F): Client receives the authorization code and retrieves the previously stored
code_verifier
from the session. - (G): Client sends a token request to Keycloak's token endpoint, including the authorization code and the
code_verifier
. - (H): Keycloak verifies that the
code_verifier
matches thecode_challenge
and, upon validation, issues access (and optionally refresh) tokens. - (I): Client receives the access token from Keycloak.
- (J): Client uses the access token to access protected resources on the Resource Server.
This diagram illustrates each step of the PKCE flow, highlighting the interactions between the user's browser, the client application, Keycloak's authorization server, and the resource server. By following this sequence, PKCE ensures a secure exchange of authorization codes and access tokens, mitigating potential interception attacks.
Implementing PKCE with Keycloak involves configuring both the client application and the authentication strategy used to communicate with Keycloak. In this setup, we utilize a custom Passport.js strategy, KeycloakStrategy
, built upon the passport-oauth2
library, to handle the OAuth 2.0 flow with PKCE.
KeycloakStrategy
extends OAuth2Strategy
from passport-oauth2
to incorporate PKCE-specific parameters into the authorization and token requests. It handles:
- Authorization Parameters: Injecting
code_challenge
andcode_challenge_method
into authorization requests. - Token Parameters: Including the
code_verifier
during the token exchange. - User Profile Retrieval: Fetching and parsing user information from Keycloak's userinfo endpoint.
PKCE is automatically handled by KeycloakStrategy when enabled in the configuration:
import KeycloakStrategy from 'passport-keycloak-oauth2-oidc-portable';
passport.use(new KeycloakStrategy({
clientID: 'test-client',
realm: 'TestRealm',
publicClient: true,
authServerURL: 'http://localhost:3000',
callbackURL: 'http://localhost:3002/auth/callback',
pkce: true, // Enable PKCE
state: true // Use StateStore
}, callback));
PKCE requires session support for storing the code verifier:
import session from 'express-session';
app.use(session({
secret: 'your-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000
}
}));
// Initialize authentication
app.get('/auth/keycloak',
passport.authenticate('keycloak')
);
// Handle callback with PKCE verification
app.get('/auth/callback',
passport.authenticate('keycloak', {
successRedirect: '/profile',
failureRedirect: '/login'
})
);
- Enhanced Security: Binds the authorization request to the client by requiring the correct code verifier during the token exchange.
- No Need for Client Secrets: Ideal for public clients that cannot securely store secrets, reducing the risk associated with exposed client credentials.
- Protection Against Interception Attacks: Prevents malicious actors from using intercepted authorization codes without the correct code verifier.
- Compliance with OAuth 2.0 Best Practices: Aligns with modern security recommendations, ensuring robust authentication flows.
PKCE is particularly beneficial in scenarios where:
- Public Clients: Applications like single-page apps, mobile apps, or any client that cannot securely store a client secret.
- High-Security Applications: Systems handling sensitive user data requiring stringent security measures.
- Modern OAuth Implementations: Adopting the latest OAuth 2.0 standards to ensure compatibility and security.
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import KeycloakStrategy from 'passport-keycloak-oauth2-oidc-portable';
const app = express();
// Session configuration
app.use(session({
secret: 'your-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000
}
}));
app.use(passport.initialize());
app.use(passport.session());
// Passport configuration
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
// Strategy setup with PKCE
passport.use(new KeycloakStrategy({
clientID: 'test-client',
realm: 'TestRealm',
publicClient: true,
authServerURL: 'http://localhost:3000',
callbackURL: 'http://localhost:3002/auth/callback',
pkce: true,
state: true
}, (accessToken, refreshToken, profile, done) => {
return done(null, { ...profile, accessToken });
}));
// Routes
app.get('/auth/keycloak', passport.authenticate('keycloak'));
app.get('/auth/callback',
passport.authenticate('keycloak', {
successRedirect: '/profile',
failureRedirect: '/login'
})
);
app.get('/profile',
ensureAuthenticated,
(req, res) => {
res.json({ user: req.user });
}
);
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect('/auth/keycloak');
}
app.listen(3002);