oauth2-oidc-implementer
SKILL.md
OAuth 2.0 & OIDC Implementer
Implement secure authentication with OAuth 2.0 and OpenID Connect.
Core Workflow
- Choose flow: Authorization Code, PKCE, Client Credentials
- Configure provider: Set up OAuth/OIDC provider
- Implement flow: Handle redirects and tokens
- Secure tokens: Storage and refresh
- Add providers: Multiple identity providers
- Handle sessions: Manage authenticated state
OAuth 2.0 Flows Overview
┌─────────────────────────────────────────────────────────────┐
│ OAuth 2.0 Flows │
├─────────────────────────────────────────────────────────────┤
│ Authorization Code + PKCE │ Web/Mobile apps (recommended) │
│ Client Credentials │ Machine-to-machine │
│ Device Code │ TV/IoT devices │
│ Implicit (deprecated) │ Do not use │
└─────────────────────────────────────────────────────────────┘
Authorization Code Flow with PKCE
Server Implementation (Next.js)
// lib/auth/oauth.ts
import { randomBytes, createHash } from 'crypto';
interface OAuthConfig {
clientId: string;
clientSecret: string;
authorizationUrl: string;
tokenUrl: string;
redirectUri: string;
scopes: string[];
}
const config: OAuthConfig = {
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
authorizationUrl: 'https://provider.com/oauth/authorize',
tokenUrl: 'https://provider.com/oauth/token',
redirectUri: process.env.OAUTH_REDIRECT_URI!,
scopes: ['openid', 'profile', 'email'],
};
// Generate PKCE challenge
function generateCodeVerifier(): string {
return randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier: string): string {
return createHash('sha256').update(verifier).digest('base64url');
}
// Generate state for CSRF protection
function generateState(): string {
return randomBytes(16).toString('hex');
}
export function getAuthorizationUrl(): {
url: string;
state: string;
codeVerifier: string;
} {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
response_type: 'code',
scope: config.scopes.join(' '),
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
return {
url: `${config.authorizationUrl}?${params}`,
state,
codeVerifier,
};
}
export async function exchangeCodeForTokens(
code: string,
codeVerifier: string
): Promise<TokenResponse> {
const response = await fetch(config.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.clientId,
client_secret: config.clientSecret,
code,
redirect_uri: config.redirectUri,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
const error = await response.json();
throw new OAuthError(error.error_description || 'Token exchange failed');
}
return response.json();
}
interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token?: string;
id_token?: string;
scope: string;
}
Login Route
// app/api/auth/login/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { getAuthorizationUrl } from '@/lib/auth/oauth';
export async function GET() {
const { url, state, codeVerifier } = getAuthorizationUrl();
// Store state and verifier in secure, httpOnly cookies
const cookieStore = cookies();
cookieStore.set('oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 600, // 10 minutes
path: '/',
});
cookieStore.set('code_verifier', codeVerifier, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 600,
path: '/',
});
return NextResponse.redirect(url);
}
Callback Route
// app/api/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { exchangeCodeForTokens } from '@/lib/auth/oauth';
import { createSession } from '@/lib/auth/session';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
// Handle OAuth errors
if (error) {
const errorDescription = searchParams.get('error_description');
return NextResponse.redirect(
new URL(`/login?error=${encodeURIComponent(errorDescription || error)}`, request.url)
);
}
// Validate state
const cookieStore = cookies();
const storedState = cookieStore.get('oauth_state')?.value;
const codeVerifier = cookieStore.get('code_verifier')?.value;
if (!state || state !== storedState) {
return NextResponse.redirect(
new URL('/login?error=invalid_state', request.url)
);
}
if (!code || !codeVerifier) {
return NextResponse.redirect(
new URL('/login?error=missing_code', request.url)
);
}
try {
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code, codeVerifier);
// Create session with tokens
await createSession(tokens);
// Clear OAuth cookies
cookieStore.delete('oauth_state');
cookieStore.delete('code_verifier');
return NextResponse.redirect(new URL('/dashboard', request.url));
} catch (error) {
console.error('OAuth callback error:', error);
return NextResponse.redirect(
new URL('/login?error=authentication_failed', request.url)
);
}
}
OpenID Connect Integration
OIDC Discovery
// lib/auth/oidc.ts
interface OIDCConfig {
issuer: string;
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint: string;
jwks_uri: string;
scopes_supported: string[];
response_types_supported: string[];
}
let cachedConfig: OIDCConfig | null = null;
export async function discoverOIDCConfig(issuer: string): Promise<OIDCConfig> {
if (cachedConfig) return cachedConfig;
const response = await fetch(`${issuer}/.well-known/openid-configuration`);
if (!response.ok) {
throw new Error('Failed to fetch OIDC configuration');
}
cachedConfig = await response.json();
return cachedConfig;
}
ID Token Validation
// lib/auth/jwt.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { discoverOIDCConfig } from './oidc';
interface IDTokenClaims {
iss: string;
sub: string;
aud: string | string[];
exp: number;
iat: number;
nonce?: string;
email?: string;
email_verified?: boolean;
name?: string;
picture?: string;
}
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
export async function verifyIdToken(
idToken: string,
expectedNonce?: string
): Promise<IDTokenClaims> {
const config = await discoverOIDCConfig(process.env.OIDC_ISSUER!);
if (!jwks) {
jwks = createRemoteJWKSet(new URL(config.jwks_uri));
}
const { payload } = await jwtVerify(idToken, jwks, {
issuer: config.issuer,
audience: process.env.OAUTH_CLIENT_ID!,
});
// Verify nonce if provided (for implicit/hybrid flows)
if (expectedNonce && payload.nonce !== expectedNonce) {
throw new Error('Invalid nonce');
}
return payload as IDTokenClaims;
}
User Info Endpoint
// lib/auth/userinfo.ts
interface UserInfo {
sub: string;
email?: string;
email_verified?: boolean;
name?: string;
given_name?: string;
family_name?: string;
picture?: string;
locale?: string;
}
export async function fetchUserInfo(accessToken: string): Promise<UserInfo> {
const config = await discoverOIDCConfig(process.env.OIDC_ISSUER!);
const response = await fetch(config.userinfo_endpoint, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch user info');
}
return response.json();
}
Session Management
// lib/auth/session.ts
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';
const SESSION_SECRET = new TextEncoder().encode(process.env.SESSION_SECRET!);
interface Session {
userId: string;
email: string;
accessToken: string;
refreshToken?: string;
expiresAt: number;
}
export async function createSession(tokens: TokenResponse): Promise<void> {
const claims = await verifyIdToken(tokens.id_token!);
const session: Session = {
userId: claims.sub,
email: claims.email!,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
};
const jwt = await new SignJWT(session)
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(SESSION_SECRET);
cookies().set('session', jwt, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
});
}
export async function getSession(): Promise<Session | null> {
const sessionCookie = cookies().get('session')?.value;
if (!sessionCookie) return null;
try {
const { payload } = await jwtVerify(sessionCookie, SESSION_SECRET);
return payload as Session;
} catch {
return null;
}
}
export async function refreshSession(): Promise<Session | null> {
const session = await getSession();
if (!session?.refreshToken) return null;
// Check if access token is expired
if (session.expiresAt > Date.now() + 60000) {
return session; // Still valid
}
// Refresh tokens
const tokens = await refreshAccessToken(session.refreshToken);
await createSession(tokens);
return getSession();
}
async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
const response = await fetch(config.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: config.clientId,
client_secret: config.clientSecret,
refresh_token: refreshToken,
}),
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
return response.json();
}
Multiple Providers
// lib/auth/providers.ts
interface OAuthProvider {
id: string;
name: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl: string;
clientId: string;
clientSecret: string;
scopes: string[];
mapUserInfo: (data: any) => UserProfile;
}
export const providers: Record<string, OAuthProvider> = {
google: {
id: 'google',
name: 'Google',
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scopes: ['openid', 'email', 'profile'],
mapUserInfo: (data) => ({
id: data.sub,
email: data.email,
name: data.name,
image: data.picture,
}),
},
github: {
id: 'github',
name: 'GitHub',
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
userInfoUrl: 'https://api.github.com/user',
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scopes: ['read:user', 'user:email'],
mapUserInfo: (data) => ({
id: String(data.id),
email: data.email,
name: data.name || data.login,
image: data.avatar_url,
}),
},
microsoft: {
id: 'microsoft',
name: 'Microsoft',
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
scopes: ['openid', 'email', 'profile', 'User.Read'],
mapUserInfo: (data) => ({
id: data.id,
email: data.mail || data.userPrincipalName,
name: data.displayName,
image: null,
}),
},
};
Client Credentials Flow
// lib/auth/machine.ts
interface ClientCredentialsConfig {
tokenUrl: string;
clientId: string;
clientSecret: string;
scopes: string[];
}
let cachedToken: { token: string; expiresAt: number } | null = null;
export async function getMachineToken(config: ClientCredentialsConfig): Promise<string> {
// Check cache
if (cachedToken && cachedToken.expiresAt > Date.now() + 60000) {
return cachedToken.token;
}
const response = await fetch(config.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(
`${config.clientId}:${config.clientSecret}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: config.scopes.join(' '),
}),
});
if (!response.ok) {
throw new Error('Failed to get machine token');
}
const data = await response.json();
cachedToken = {
token: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return cachedToken.token;
}
Best Practices
- Always use PKCE: Even for confidential clients
- Validate state: Prevent CSRF attacks
- Verify tokens: Check signature and claims
- Secure storage: HttpOnly cookies for tokens
- Refresh proactively: Before expiration
- Handle errors gracefully: Clear messaging
- Use HTTPS: Always in production
- Limit scopes: Request minimum needed
Output Checklist
Every OAuth/OIDC implementation should include:
- PKCE code verifier/challenge
- State parameter for CSRF
- Secure token storage
- Token refresh mechanism
- ID token validation
- Session management
- Logout handling
- Error handling
- Multiple provider support
- HTTPS enforcement
Weekly Installs
13
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
claude-code10
gemini-cli8
antigravity8
github-copilot8
codex8
trae8