关于 oauth-oidc-implementer
oauth-oidc-implementer 是面向开发者的技能,用于实现 OAuth 2.0 和 OpenID Connect 身份验证。内容涵盖常见授权流程、令牌处理、身份提供方配置,以及客户端和服务端的安全检查。
主要功能
- 01OAuth 2.0 流程实现
- 02OIDC 声明与发现机制
- 03JWT 验证及令牌处理
- 04令牌刷新、撤销与注销模式
- 05PKCE、state 与 nonce 防护
- 06身份提供方配置
适用场景
- 01为应用添加社交登录
- 02使用 OAuth 2.0 授权 API 请求
- 03配置基于 OIDC 的企业 SSO
- 04排查令牌验证或刷新问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill oauth-oidc-implementer安装后在 Claude Code 中输入/oauth-oidc-implementer确认已加载。
附带文件.claude-plugin/plugin.json
详细介绍
OAuth/OIDC 实施专家
概述
擅长实施 OAuth 2.0 和 OpenID Connect(OIDC)身份验证流程。专精于安全令牌处理、社交登录集成、API 授权和身份提供商配置。遵循安全最佳实践,处理客户端和服务端流程。
适用场景
- 实施“使用 Google/GitHub 等账号登录”的社交登录
- 设置用于 API 授权的 OAuth 2.0
- 配置用于企业 SSO 的 OIDC
- 设计令牌刷新和会话管理
- 为移动应用/SPA 实施 PKCE
- 使用 JWT 验证保护 API 端点
- 与身份提供商集成(Auth0、Okta、Keycloak)
- 排查 OAuth 流程故障
能力
OAuth 2.0 流程
- 授权码流程(搭配 PKCE)
- 用于服务间通信的客户端凭据流程
- 隐式流程(旧版;了解为何应避免使用)
- 用于 IoT 的设备授权流程
- 刷新令牌轮换
OpenID Connect
- ID 令牌验证和声明
- UserInfo 端点的使用
- 发现文档(
.well-known/openid-configuration) - 会话管理(前通道/后通道注销)
- 面向公共客户端的 PKCE
令牌管理
- JWT 结构(标头、载荷、签名)
- 访问令牌、ID 令牌和刷新令牌的区别
- 令牌存储策略(httpOnly Cookie 与 localStorage)
- 令牌刷新模式
- 撤销和注销
安全最佳实践
- PKCE(代码交换证明密钥)
- 用于防范 CSRF 的 state 参数
- 用于防重放的 nonce
- 安全的令牌存储
- 短时效访问令牌
身份提供商
- Auth0 集成
- Okta 配置
- Keycloak 可自托管
- Google/GitHub/Microsoft 社交登录
- SAML 到 OIDC 的桥接
依赖项
可与以下工具配合使用:
nextjs-app-router-expert- 全栈身份验证实施api-architect- API 授权设计cloudflare-worker-dev- 边缘身份验证site-reliability-engineer- 身份验证监控
示例
搭配 PKCE 的授权码流程(SPA)
// 1. Generate PKCE challenge
function generatePKCE() {
const verifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64URLEncode(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
);
return { verifier, challenge };
}
// 2. Redirect to authorization endpoint
function initiateLogin() {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomUUID();
// Store verifier and state for later validation
sessionStorage.setItem('pkce_verifier', verifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: 'your-client-id',
redirect_uri: 'https://yourapp.com/callback',
scope: 'openid profile email',
state: state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
window.location.href = `https://auth.example.com/authorize?${params}`;
}
// 3. Handle callback and exchange code for tokens
async function handleCallback(code: string, state: string) {
// Validate state
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch - possible CSRF attack');
}
const verifier = sessionStorage.getItem('pkce_verifier');
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'your-client-id',
code_verifier: verifier,
}),
});
const tokens = await response.json();
// { access_token, id_token, refresh_token, expires_in }
// Clean up
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
return tokens;
}
Next.js API 路由令牌处理程序
// app/api/auth/callback/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const state = searchParams.get('state');
// Validate state from cookie
const cookieStore = cookies();
const storedState = cookieStore.get('oauth_state')?.value;
if (state !== storedState) {
return NextResponse.redirect('/auth/error?reason=state_mismatch');
}
// Exchange code for tokens (server-side, can use client_secret)
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code!,
redirect_uri: process.env.REDIRECT_URI!,
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
}),
});
const tokens = await tokenResponse.json();
// Store refresh token in httpOnly cookie
const response = NextResponse.redirect('/dashboard');
response.cookies.set('refresh_token', tokens.refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 days
path: '/',
});
// Access token can go to client or httpOnly cookie
response.cookies.set('access_token', tokens.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: tokens.expires_in,
path: '/',
});
return response;
}
JWT 验证(Node.js)
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
rateLimit: true,
});
function getKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.getPublicKey();
callback(err, signingKey);
});
}
export async function validateToken(token: string): Promise<JWTPayload> {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
algorithms: ['RS256'],
issuer: 'https://auth.example.com/',
audience: 'your-client-id',
},
(err, decoded) => {
if (err) reject(err);
else resolve(decoded as JWTPayload);
}
);
});
}
// Middleware usage
export async function authMiddleware(req: Request) {
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
try {
const payload = await validateToken(token);
// Attach user to request context
return { user: payload };
} catch (error) {
return new Response('Invalid token', { status: 401 });
}
}
令牌刷新模式
// Client-side token refresh with race condition handling
let refreshPromise: Promise<string> | null = null;
async function getAccessToken(): Promise<string> {
const accessToken = localStorage.getItem('access_token');
const expiresAt = localStorage.getItem('token_expires_at');
// Check if token is still valid (with 60s buffer)
if (accessToken && expiresAt && Date.now() < parseInt(expiresAt) - 60000) {
return accessToken;
}
// Deduplicate concurrent refresh requests
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = refreshAccessToken();
try {
return await refreshPromise;
} finally {
refreshPromise = null;
}
}
async function refreshAccessToken(): Promise<string> {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // Send httpOnly refresh token cookie
});
if (!response.ok) {
// Refresh failed, redirect to login
window.location.href = '/login';
throw new Error('Token refresh failed');
}
const { access_token, expires_in } = await response.json();
localStorage.setItem('access_token', access_token);
localStorage.setItem('token_expires_at', String(Date.now() + expires_in * 1000));
return access_token;
}
社交登录设置(Auth0)
// auth0.config.ts
export const auth0Config = {
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
redirectUri: process.env.AUTH0_REDIRECT_URI!,
scope: 'openid profile email',
audience: process.env.AUTH0_AUDIENCE, // For API access
};
// Login URL builder
export function getLoginUrl(connection?: string) {
const params = new URLSearchParams({
response_type: 'code',
client_id: auth0Config.clientId,
redirect_uri: auth0Config.redirectUri,
scope: auth0Config.scope,
state: generateState(),
...(auth0Config.audience && { audience: auth0Config.audience }),
...(connection && { connection }), // 'google-oauth2', 'github', etc.
});
return `https://${auth0Config.domain}/authorize?${params}`;
}
OIDC 发现
// Fetch and cache OIDC configuration
interface OIDCConfig {
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint: string;
jwks_uri: string;
issuer: string;
}
let oidcConfig: OIDCConfig | null = null;
export async function getOIDCConfig(issuer: string): Promise<OIDCConfig> {
if (oidcConfig) return oidcConfig;
const response = await fetch(`${issuer}/.well-known/openid-configuration`);
oidcConfig = await response.json();
return oidcConfig;
}
最佳实践
- 始终使用 PKCE - 即使是机密客户端,也能增强安全性
- 验证 state 参数 - 防止 CSRF 攻击
- 使用 httpOnly Cookie - 刷新令牌应存放于此,绝不要存入 localStorage
- 使用短时效访问令牌 - 常见时长为 15 分钟,按需刷新
- 在服务端验证令牌 - 不要只信任客户端验证
- 使用 nonce 声明 - 防止 ID 令牌遭受重放攻击
- 正确实施注销 - 撤销令牌并清除会话
- 验证受众和签发者 - 确保令牌是为你的应用签发的
- 轮换刷新令牌 - 每次使用时签发新的刷新令牌
常见陷阱
- 将令牌存储在 localStorage 中 - 容易受到 XSS 攻击
- 不验证 state - 会造成 CSRF 漏洞
- 在 SPA 中使用隐式流程 - 已弃用,请使用授权码流程 + PKCE
- 访问令牌有效期过长 - 令牌泄露时风险更高
- 不验证 JWT 签名 - 任何人都能伪造未签名的令牌
- 硬编码客户端密钥 - 应使用环境变量
- 未实现令牌撤销 - 用户无法正确注销
- 未处理令牌过期 - 静默刷新失败会导致糟糕的用户体验
评论
支持 Markdown