Implementing OAuth
Quick Start
import crypto from "crypto";
export function generatePKCE() {
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
const state = crypto.randomBytes(16).toString("hex");
return { codeVerifier, codeChallenge, state };
}
export function buildAuthUrl(config: OAuthConfig, pkce: PKCEPair) {
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
response_type: "code",
scope: config.scopes.join(" "),
state: pkce.state,
code_challenge: pkce.codeChallenge,
code_challenge_method: "S256",
});
return `${config.authorizationEndpoint}?${params}`;
}
Features
| Feature |
Description |
Reference |
| Authorization Code + PKCE |
Secure flow for public/confidential clients |
RFC 7636 |
| Token Management |
Access/refresh token handling and storage |
RFC 6749 |
| OpenID Connect |
Identity layer with ID tokens and claims |
OIDC Core |
| Provider Integration |
Google, GitHub, Microsoft configurations |
OIDC Discovery |
| JWT Validation |
ID token signature and claims verification |
RFC 7519 |
Common Patterns
Token Exchange
async function exchangeCodeForTokens(code: string, codeVerifier: string) {
const response = await fetch(config.tokenEndpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
code_verifier: codeVerifier,
}),
});
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
idToken: data.id_token,
};
}
Provider Configuration
const googleConfig = {
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
tokenEndpoint: "https://oauth2.googleapis.com/token",
userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo",
scopes: ["openid", "email", "profile"],
};
const githubConfig = {
authorizationEndpoint: "https://github.com/login/oauth/authorize",
tokenEndpoint: "https://github.com/login/oauth/access_token",
userInfoEndpoint: "https://api.github.com/user",
scopes: ["read:user", "user:email"],
};
Token Refresh Middleware
async function ensureFreshToken(req: Request, res: Response, next: NextFunction) {
const token = await tokenStore.get(req.session.userId);
if (!token) return next();
const timeUntilExpiry = token.expiresAt - Date.now();
if (timeUntilExpiry > 5 * 60 * 1000) {
req.accessToken = token.accessToken;
return next();
}
const newTokens = await client.refreshTokens(token.refreshToken);
await tokenStore.save(req.session.userId, {
...newTokens,
expiresAt: Date.now() + newTokens.expiresIn * 1000,
});
req.accessToken = newTokens.accessToken;
next();
}
Best Practices
| Do |
Avoid |
| Always use PKCE for authorization code flow |
Using implicit flow for new apps |
| Validate state parameter to prevent CSRF |
Storing tokens in localStorage |
| Store tokens securely (encrypted, httpOnly) |
Exposing client secrets in frontend |
| Implement token refresh before expiration |
Ignoring token expiration |
| Validate ID token signatures with JWKS |
Trusting unverified ID tokens |
| Use short-lived access tokens |
Reusing authorization codes |
References