oauth2-oidc-implementer

安装量: 77
排名: #10138

安装

npx skills add https://github.com/patricio0312rev/skills --skill oauth2-oidc-implementer

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 { 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 { 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 | null = null;

export async function verifyIdToken( idToken: string, expectedNonce?: string ): Promise { 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 { 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 { 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 { 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 { 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 { 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 = { 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 { // 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

返回排行榜