oauth2-authentication by manutej/luxor-claude-marketplace
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill oauth2-authentication一个使用 OAuth2 和 OpenID Connect 实现安全认证和授权的综合性技能。此技能涵盖所有主要的授权流程、令牌管理策略、安全最佳实践以及适用于 Web、移动和 API 应用程序的实际实现模式。
在以下情况下使用此技能:
OAuth2 是一个授权框架,使应用程序能够获取对 HTTP 服务上用户帐户的有限访问权限。它的工作原理是将用户认证委托给托管用户帐户的服务,并授权第三方应用程序访问该帐户。
关键术语:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
OAuth2 为不同的用例定义了几种授权类型:
最安全的流程 - 推荐用于服务器端应用程序
授权码流程是最安全且使用最广泛的 OAuth2 流程。它涉及在服务器端用授权码交换访问令牌。
流程步骤:
何时使用:
安全优势:
适用于公共客户端(SPA 和移动应用)的安全流程
PKCE(Proof Key for Code Exchange,发音为 "pixy")是授权码流程的扩展,专为无法安全存储客户端密钥的公共客户端设计。
流程步骤:
何时使用:
安全优势:
机器对机器认证
当客户端本身就是资源所有者时,使用客户端凭证流程,通常用于服务到服务的通信。
流程步骤:
何时使用:
特点:
遗留流程 - 不再推荐
隐式流程直接在 URL 片段中返回令牌,无需授权码交换。此流程现在被认为不安全,应避免使用。
为何弃用:
遗留流程 - 除非必要,否则避免使用
资源所有者密码凭证流程允许客户端直接收集用户名和密码,然后将它们交换为令牌。
流程步骤:
何时使用(罕见):
为何避免:
适用于输入受限的设备
设备流程专为输入能力有限的设备(智能电视、物联网设备、CLI 工具)设计。
流程步骤:
何时使用:
用于访问受保护资源的短期凭证
特点:
Authorization: Bearer <access_token>JWT 结构(当使用 JWT 时):
Header.Payload.Signature
JWT 负载声明:
sub:主体(用户 ID)iat:颁发时间exp:过期时间iss:颁发者(授权服务器)aud:受众(资源服务器)scope:授予的权限令牌验证:
用于获取新访问令牌的长期凭证
特点:
刷新令牌轮换:
令牌存储最佳实践:
Web 应用程序:
移动应用程序:
SPA:
包含用户身份信息的令牌
特点:
标准声明:
sub:主体(唯一用户 ID)name:全名email:电子邮件地址email_verified:电子邮件验证状态picture:个人资料图片 URLiat、exp:颁发/过期时间用于访问控制的细粒度权限
作用域定义了客户端请求的访问权限以及访问令牌允许的权限。
作用域命名约定:
read:users - 读取用户数据write:users - 创建/更新用户delete:users - 删除用户admin:all - 完全管理访问权限openid - 请求 OpenID Connect ID 令牌profile - 访问用户配置文件信息email - 访问用户电子邮件地址最佳实践:
动态作用域:
read:organization:{org_id}
write:project:{project_id}
admin:tenant:{tenant_id}
防止跨站请求伪造(CSRF)
状态参数是客户端在授权请求中包含并在回调中验证的随机值。
实现:
状态值示例:
state: crypto.randomBytes(32).toString('hex')
// "7f8a3d9e2b1c4f5a6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
Proof Key for Code Exchange - 防止代码拦截
PKCE 保护授权码流程免受授权码拦截攻击。
代码验证器:
代码挑战:
代码挑战方法:
S256:SHA256 哈希(使用此方法)plain:纯文本验证器(仅限遗留用途)实现示例:
// Generate code verifier
const codeVerifier = generateRandomString(64);
// Generate code challenge
const codeChallenge = base64UrlEncode(
sha256(codeVerifier)
);
// Store code verifier for token exchange
sessionStorage.setItem('code_verifier', codeVerifier);
// Include in authorization URL
const authUrl = `${authEndpoint}?` +
`client_id=${clientId}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code` +
`&scope=${scopes}` +
`&state=${state}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256`;
保护访问令牌和刷新令牌
应做:
不应做:
防止开放重定向漏洞
严格验证规则:
移动端深度链接:
com.example.app://callbackhttps://example.com/auth/callbackhttps://example.com/auth/callback本地开发:
http://localhost:3000/callback构建在 OAuth2 之上的身份层
OpenID Connect 在 OAuth2 之上添加了一个身份层,除了授权之外还提供认证。
与 OAuth2 的主要区别:
OIDC 流程:
授权码流程(推荐)
隐式流程(已弃用)
混合流程
OIDC 作用域:
openid(必需)- 启用 OIDCprofile - 姓名、图片、区域设置等email - 电子邮件地址和验证状态address - 物理地址phone - 电话号码用户信息端点:
GET /userinfo
Authorization: Bearer <access_token>
Response:
{
"sub": "248289761001",
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified": true,
"picture": "https://example.com/photo.jpg"
}
ID 令牌验证:
组织特定的认证
许多 SaaS 应用程序要求用户在组织或租户的上下文中进行认证。
模式:
组织参数
scope=openid profile organization:acme-corp组织选择器
每个租户的自定义域
acme.example.com 与 globex.example.com令牌声明中的组织
适用于带后端的传统 Web 应用程序的完整实现
// OAuth2 Configuration
const oauth2Config = {
clientId: process.env.OAUTH2_CLIENT_ID,
clientSecret: process.env.OAUTH2_CLIENT_SECRET,
authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
tokenEndpoint: 'https://auth.example.com/oauth/token',
redirectUri: 'https://yourapp.com/auth/callback',
scopes: ['openid', 'profile', 'email', 'read:data'],
// Optional OIDC endpoints
userInfoEndpoint: 'https://auth.example.com/oauth/userinfo',
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
// Security settings
useStateParameter: true,
usePKCE: false, // Not needed for confidential clients
};
const crypto = require('crypto');
function generateAuthorizationUrl(req) {
// Generate and store state for CSRF protection
const state = crypto.randomBytes(32).toString('hex');
req.session.oauth2State = state;
// Build authorization URL
const params = new URLSearchParams({
client_id: oauth2Config.clientId,
redirect_uri: oauth2Config.redirectUri,
response_type: 'code',
scope: oauth2Config.scopes.join(' '),
state: state,
});
return `${oauth2Config.authorizationEndpoint}?${params.toString()}`;
}
// Express.js route
app.get('/auth/login', (req, res) => {
const authUrl = generateAuthorizationUrl(req);
res.redirect(authUrl);
});
const axios = require('axios');
async function exchangeCodeForToken(code) {
const response = await axios.post(
oauth2Config.tokenEndpoint,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: oauth2Config.redirectUri,
client_id: oauth2Config.clientId,
client_secret: oauth2Config.clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return response.data;
// Returns: { access_token, refresh_token, token_type, expires_in, id_token }
}
// Callback route
app.get('/auth/callback', async (req, res) => {
const { code, state, error, error_description } = req.query;
// Check for authorization errors
if (error) {
console.error('Authorization error:', error, error_description);
return res.redirect('/auth/error?message=' + error_description);
}
// Validate state parameter (CSRF protection)
if (state !== req.session.oauth2State) {
console.error('State mismatch - possible CSRF attack');
return res.status(403).send('Invalid state parameter');
}
// Clear state from session
delete req.session.oauth2State;
try {
// Exchange authorization code for tokens
const tokens = await exchangeCodeForToken(code);
// Store tokens securely in session
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.tokenExpiry = Date.now() + (tokens.expires_in * 1000);
// Optional: Fetch user info
if (tokens.id_token) {
const userInfo = await getUserInfo(tokens.access_token);
req.session.user = userInfo;
}
// Redirect to application
res.redirect('/dashboard');
} catch (error) {
console.error('Token exchange failed:', error);
res.redirect('/auth/error?message=Authentication failed');
}
});
// Middleware to check authentication
function requireAuth(req, res, next) {
if (!req.session.accessToken) {
return res.redirect('/auth/login');
}
// Check if token is expired
if (Date.now() > req.session.tokenExpiry) {
// Token expired, try to refresh
return refreshAccessToken(req, res, next);
}
next();
}
// API request with access token
async function fetchUserData(accessToken) {
const response = await axios.get('https://api.example.com/user/data', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
return response.data;
}
// Protected route
app.get('/dashboard', requireAuth, async (req, res) => {
try {
const userData = await fetchUserData(req.session.accessToken);
res.render('dashboard', { user: req.session.user, data: userData });
} catch (error) {
console.error('API request failed:', error);
res.status(500).send('Failed to fetch data');
}
});
async function refreshAccessToken(req, res, next) {
if (!req.session.refreshToken) {
// No refresh token, require re-authentication
return res.redirect('/auth/login');
}
try {
const response = await axios.post(
oauth2Config.tokenEndpoint,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: req.session.refreshToken,
client_id: oauth2Config.clientId,
client_secret: oauth2Config.clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
// Update tokens in session
req.session.accessToken = response.data.access_token;
req.session.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
// Update refresh token if rotation is enabled
if (response.data.refresh_token) {
req.session.refreshToken = response.data.refresh_token;
}
next();
} catch (error) {
console.error('Token refresh failed:', error);
// Refresh failed, require re-authentication
delete req.session.accessToken;
delete req.session.refreshToken;
res.redirect('/auth/login');
}
}
app.post('/auth/logout', async (req, res) => {
// Optional: Revoke tokens on authorization server
if (req.session.accessToken) {
try {
await revokeToken(req.session.accessToken);
} catch (error) {
console.error('Token revocation failed:', error);
}
}
// Clear session
req.session.destroy((err) => {
if (err) {
console.error('Session destruction failed:', err);
}
res.redirect('/');
});
});
async function revokeToken(token) {
await axios.post(
'https://auth.example.com/oauth/revoke',
new URLSearchParams({
token: token,
client_id: oauth2Config.clientId,
client_secret: oauth2Config.clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
}
适用于 React SPA 的完整 OAuth2 与 PKCE 实现
// src/config/oauth2.js
export const oauth2Config = {
clientId: process.env.REACT_APP_OAUTH2_CLIENT_ID,
authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
tokenEndpoint: 'https://auth.example.com/oauth/token',
redirectUri: window.location.origin + '/auth/callback',
scopes: ['openid', 'profile', 'email', 'read:data'],
audience: 'https://api.example.com',
};
// PKCE utility functions
export function generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const values = crypto.getRandomValues(new Uint8Array(length));
return Array.from(values)
.map(v => charset[v % charset.length])
.join('');
}
export async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(digest);
}
export function base64UrlEncode(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// src/contexts/AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import { oauth2Config, generateRandomString, generateCodeChallenge } from '../config/oauth2';
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [accessToken, setAccessToken] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing session
checkAuth();
}, []);
async function checkAuth() {
// Try to restore access token from memory or refresh
const storedToken = sessionStorage.getItem('access_token');
const expiresAt = sessionStorage.getItem('expires_at');
if (storedToken && expiresAt && Date.now() < parseInt(expiresAt)) {
setAccessToken(storedToken);
await fetchUserInfo(storedToken);
} else {
// Token expired or doesn't exist, try refresh
await tryRefresh();
}
setLoading(false);
}
async function login() {
// Generate PKCE parameters
const codeVerifier = generateRandomString(64);
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateRandomString(32);
// Store for callback
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('oauth2_state', state);
// Build authorization URL
const params = new URLSearchParams({
client_id: oauth2Config.clientId,
redirect_uri: oauth2Config.redirectUri,
response_type: 'code',
scope: oauth2Config.scopes.join(' '),
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
// Redirect to authorization server
window.location.href = `${oauth2Config.authorizationEndpoint}?${params}`;
}
async function handleCallback(code, state) {
// Validate state
const savedState = sessionStorage.getItem('oauth2_state');
if (state !== savedState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
// Get code verifier
const codeVerifier = sessionStorage.getItem('code_verifier');
if (!codeVerifier) {
throw new Error('Code verifier not found');
}
// Exchange code for tokens
const response = await fetch(oauth2Config.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: oauth2Config.redirectUri,
client_id: oauth2Config.clientId,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
// Store tokens
setAccessToken(tokens.access_token);
sessionStorage.setItem('access_token', tokens.access_token);
sessionStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
// Store refresh token in httpOnly cookie via backend
if (tokens.refresh_token) {
await storeRefreshToken(tokens.refresh_token);
}
// Fetch user info
await fetchUserInfo(tokens.access_token);
// Clean up
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('oauth2_state');
}
async function fetchUserInfo(token) {
const response = await fetch('https://auth.example.com/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const userInfo = await response.json();
setUser(userInfo);
}
}
async function storeRefreshToken(refreshToken) {
// Store refresh token via backend (httpOnly cookie)
await fetch('/api/auth/store-refresh-token', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken }),
});
}
async function tryRefresh() {
try {
// Call backend to refresh using httpOnly cookie
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
const tokens = await response.json();
setAccessToken(tokens.access_token);
sessionStorage.setItem('access_token', tokens.access_token);
sessionStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
await fetchUserInfo(tokens.access_token);
return true;
}
} catch (error) {
console.error('Token refresh failed:', error);
}
return false;
}
async function logout() {
// Revoke tokens
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
// Clear state
setUser(null);
setAccessToken(null);
sessionStorage.clear();
}
const value = {
user,
accessToken,
loading,
login,
logout,
handleCallback,
isAuthenticated: !!accessToken,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// src/components/AuthCallback.js
import React, { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function AuthCallback() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { handleCallback } = useAuth();
const [error, setError] = useState(null);
useEffect(() => {
const code = searchParams.get('code');
const state = searchParams.get('state');
const errorParam = searchParams.get('error');
const errorDescription = searchParams.get('error_description');
if (errorParam) {
setError(errorDescription || errorParam);
return;
}
if (code && state) {
handleCallback(code, state)
.then(() => {
navigate('/dashboard');
})
.catch((err) => {
console.error('Authentication failed:', err);
setError(err.message);
});
} else {
setError('Missing authorization code or state');
}
}, [searchParams, handleCallback, navigate]);
if (error) {
return (
<div className="auth-error">
<h2>Authentication Failed</h2>
<p>{error}</p>
<button onClick={() => navigate('/')}>Return Home</button>
</div>
);
}
return (
<div className="auth-loading">
<h2>Completing authentication...</h2>
<div className="spinner"></div>
</div>
);
}
// src/components/ProtectedRoute.js
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
// src/utils/apiClient.js
import { oauth2Config } from '../config/oauth2';
class ApiClient {
constructor() {
this.baseUrl = 'https://api.example.com';
}
async request(endpoint, options = {}) {
const accessToken = sessionStorage.getItem('access_token');
const expiresAt = sessionStorage.getItem('expires_at');
// Check if token needs refresh
if (!accessToken || Date.now() >= parseInt(expiresAt)) {
await this.refreshToken();
}
const token = sessionStorage.getItem('access_token');
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
// Token invalid, try refresh once
await this.refreshToken();
const newToken = sessionStorage.getItem('access_token');
// Retry request
const retryResponse = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`,
'Content-Type': 'application/json',
},
});
if (!retryResponse.ok) {
throw new Error('API request failed after token refresh');
}
return retryResponse.json();
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
async refreshToken() {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
// Refresh failed, redirect to login
window.location.href = '/login';
throw new Error('Token refresh failed');
}
const tokens = await response.json();
sessionStorage.setItem('access_token', tokens.access_token);
sessionStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
}
async get(endpoint) {
return this.request(endpoint);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async delete(endpoint) {
return this.request(endpoint, {
A comprehensive skill for implementing secure authentication and authorization using OAuth2 and OpenID Connect. This skill covers all major authorization flows, token management strategies, security best practices, and real-world implementation patterns for web, mobile, and API applications.
Use this skill when:
OAuth2 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service. It works by delegating user authentication to the service that hosts the user account and authorizing third-party applications to access that account.
Key Terminology:
OAuth2 defines several grant types for different use cases:
Most Secure Flow - Recommended for Server-Side Applications
The authorization code flow is the most secure and widely used OAuth2 flow. It involves exchanging an authorization code for an access token on the server side.
Flow Steps:
When to Use:
Security Benefits:
Secure Flow for Public Clients (SPAs and Mobile Apps)
PKCE (Proof Key for Code Exchange, pronounced "pixy") is an extension to the authorization code flow designed for public clients that cannot securely store client secrets.
Flow Steps:
When to Use:
Security Benefits:
Machine-to-Machine Authentication
The client credentials flow is used when the client itself is the resource owner, typically for service-to-service communication.
Flow Steps:
When to Use:
Characteristics:
Legacy Flow - No Longer Recommended
The implicit flow returns tokens directly in the URL fragment without an authorization code exchange. This flow is now considered insecure and should be avoided.
Why Deprecated:
Legacy Flow - Avoid Unless Necessary
The resource owner password credentials flow allows the client to collect username and password directly, then exchange them for tokens.
Flow Steps:
When to Use (Rarely):
Why to Avoid:
For Input-Constrained Devices
The device flow is designed for devices with limited input capabilities (smart TVs, IoT devices, CLI tools).
Flow Steps:
When to Use:
Short-lived credentials for accessing protected resources
Characteristics:
Authorization: Bearer <access_token>JWT Structure (when using JWTs):
Header.Payload.Signature
JWT Payload Claims:
sub: Subject (user ID)iat: Issued at timeexp: Expiration timeiss: Issuer (authorization server)aud: Audience (resource server)scope: Granted permissionsToken Validation:
Long-lived credentials for obtaining new access tokens
Characteristics:
Refresh Token Rotation:
Token Storage Best Practices:
Web Applications:
Mobile Applications:
SPAs:
Tokens containing user identity information
Characteristics:
Standard Claims:
sub: Subject (unique user ID)name: Full nameemail: Email addressemail_verified: Email verification statuspicture: Profile picture URLiat, exp: Issued/expiration timesFine-grained permissions for access control
Scopes define what access the client is requesting and what the access token permits.
Scope Naming Conventions:
read:users - Read user datawrite:users - Create/update usersdelete:users - Delete usersadmin:all - Full administrative accessopenid - Request OpenID Connect ID tokenprofile - Access user profile informationemail - Access user email addressBest Practices:
Dynamic Scopes:
read:organization:{org_id}
write:project:{project_id}
admin:tenant:{tenant_id}
Prevents Cross-Site Request Forgery (CSRF)
The state parameter is a random value that the client includes in the authorization request and validates in the callback.
Implementation:
Example State Value:
state: crypto.randomBytes(32).toString('hex')
// "7f8a3d9e2b1c4f5a6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
Proof Key for Code Exchange - Prevents Code Interception
PKCE protects the authorization code flow against authorization code interception attacks.
Code Verifier:
Code Challenge:
Code Challenge Methods:
S256: SHA256 hash (use this)plain: Plaintext verifier (legacy only)Implementation Example:
// Generate code verifier
const codeVerifier = generateRandomString(64);
// Generate code challenge
const codeChallenge = base64UrlEncode(
sha256(codeVerifier)
);
// Store code verifier for token exchange
sessionStorage.setItem('code_verifier', codeVerifier);
// Include in authorization URL
const authUrl = `${authEndpoint}?` +
`client_id=${clientId}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code` +
`&scope=${scopes}` +
`&state=${state}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256`;
Protecting Access and Refresh Tokens
Do:
Don't:
Prevent Open Redirect Vulnerabilities
Strict Validation Rules:
Mobile Deep Links:
com.example.app://callbackhttps://example.com/auth/callbackhttps://example.com/auth/callbackLocalhost Development:
http://localhost:3000/callbackIdentity Layer Built on OAuth2
OpenID Connect adds an identity layer on top of OAuth2, providing authentication in addition to authorization.
Key Differences from OAuth2:
OIDC Flows:
Authorization Code Flow (recommended)
Implicit Flow (deprecated)
Hybrid Flow
OIDC Scopes:
openid (required) - Enables OIDCprofile - Name, picture, locale, etc.email - Email address and verification statusaddress - Physical addressphone - Phone numberUserInfo Endpoint:
GET /userinfo
Authorization: Bearer <access_token>
Response:
{
"sub": "248289761001",
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified": true,
"picture": "https://example.com/photo.jpg"
}
ID Token Validation:
Organization-Specific Authentication
Many SaaS applications require users to authenticate within the context of an organization or tenant.
Patterns:
Organization Parameter
scope=openid profile organization:acme-corpOrganization Selector
Custom Domain per Tenant
acme.example.com vs globex.example.comOrganization in Token Claims
Complete implementation for traditional web applications with backend
// OAuth2 Configuration
const oauth2Config = {
clientId: process.env.OAUTH2_CLIENT_ID,
clientSecret: process.env.OAUTH2_CLIENT_SECRET,
authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
tokenEndpoint: 'https://auth.example.com/oauth/token',
redirectUri: 'https://yourapp.com/auth/callback',
scopes: ['openid', 'profile', 'email', 'read:data'],
// Optional OIDC endpoints
userInfoEndpoint: 'https://auth.example.com/oauth/userinfo',
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
// Security settings
useStateParameter: true,
usePKCE: false, // Not needed for confidential clients
};
const crypto = require('crypto');
function generateAuthorizationUrl(req) {
// Generate and store state for CSRF protection
const state = crypto.randomBytes(32).toString('hex');
req.session.oauth2State = state;
// Build authorization URL
const params = new URLSearchParams({
client_id: oauth2Config.clientId,
redirect_uri: oauth2Config.redirectUri,
response_type: 'code',
scope: oauth2Config.scopes.join(' '),
state: state,
});
return `${oauth2Config.authorizationEndpoint}?${params.toString()}`;
}
// Express.js route
app.get('/auth/login', (req, res) => {
const authUrl = generateAuthorizationUrl(req);
res.redirect(authUrl);
});
const axios = require('axios');
async function exchangeCodeForToken(code) {
const response = await axios.post(
oauth2Config.tokenEndpoint,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: oauth2Config.redirectUri,
client_id: oauth2Config.clientId,
client_secret: oauth2Config.clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return response.data;
// Returns: { access_token, refresh_token, token_type, expires_in, id_token }
}
// Callback route
app.get('/auth/callback', async (req, res) => {
const { code, state, error, error_description } = req.query;
// Check for authorization errors
if (error) {
console.error('Authorization error:', error, error_description);
return res.redirect('/auth/error?message=' + error_description);
}
// Validate state parameter (CSRF protection)
if (state !== req.session.oauth2State) {
console.error('State mismatch - possible CSRF attack');
return res.status(403).send('Invalid state parameter');
}
// Clear state from session
delete req.session.oauth2State;
try {
// Exchange authorization code for tokens
const tokens = await exchangeCodeForToken(code);
// Store tokens securely in session
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.tokenExpiry = Date.now() + (tokens.expires_in * 1000);
// Optional: Fetch user info
if (tokens.id_token) {
const userInfo = await getUserInfo(tokens.access_token);
req.session.user = userInfo;
}
// Redirect to application
res.redirect('/dashboard');
} catch (error) {
console.error('Token exchange failed:', error);
res.redirect('/auth/error?message=Authentication failed');
}
});
// Middleware to check authentication
function requireAuth(req, res, next) {
if (!req.session.accessToken) {
return res.redirect('/auth/login');
}
// Check if token is expired
if (Date.now() > req.session.tokenExpiry) {
// Token expired, try to refresh
return refreshAccessToken(req, res, next);
}
next();
}
// API request with access token
async function fetchUserData(accessToken) {
const response = await axios.get('https://api.example.com/user/data', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
return response.data;
}
// Protected route
app.get('/dashboard', requireAuth, async (req, res) => {
try {
const userData = await fetchUserData(req.session.accessToken);
res.render('dashboard', { user: req.session.user, data: userData });
} catch (error) {
console.error('API request failed:', error);
res.status(500).send('Failed to fetch data');
}
});
async function refreshAccessToken(req, res, next) {
if (!req.session.refreshToken) {
// No refresh token, require re-authentication
return res.redirect('/auth/login');
}
try {
const response = await axios.post(
oauth2Config.tokenEndpoint,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: req.session.refreshToken,
client_id: oauth2Config.clientId,
client_secret: oauth2Config.clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
// Update tokens in session
req.session.accessToken = response.data.access_token;
req.session.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
// Update refresh token if rotation is enabled
if (response.data.refresh_token) {
req.session.refreshToken = response.data.refresh_token;
}
next();
} catch (error) {
console.error('Token refresh failed:', error);
// Refresh failed, require re-authentication
delete req.session.accessToken;
delete req.session.refreshToken;
res.redirect('/auth/login');
}
}
app.post('/auth/logout', async (req, res) => {
// Optional: Revoke tokens on authorization server
if (req.session.accessToken) {
try {
await revokeToken(req.session.accessToken);
} catch (error) {
console.error('Token revocation failed:', error);
}
}
// Clear session
req.session.destroy((err) => {
if (err) {
console.error('Session destruction failed:', err);
}
res.redirect('/');
});
});
async function revokeToken(token) {
await axios.post(
'https://auth.example.com/oauth/revoke',
new URLSearchParams({
token: token,
client_id: oauth2Config.clientId,
client_secret: oauth2Config.clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
}
Complete OAuth2 with PKCE implementation for React SPAs
// src/config/oauth2.js
export const oauth2Config = {
clientId: process.env.REACT_APP_OAUTH2_CLIENT_ID,
authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
tokenEndpoint: 'https://auth.example.com/oauth/token',
redirectUri: window.location.origin + '/auth/callback',
scopes: ['openid', 'profile', 'email', 'read:data'],
audience: 'https://api.example.com',
};
// PKCE utility functions
export function generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const values = crypto.getRandomValues(new Uint8Array(length));
return Array.from(values)
.map(v => charset[v % charset.length])
.join('');
}
export async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(digest);
}
export function base64UrlEncode(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// src/contexts/AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import { oauth2Config, generateRandomString, generateCodeChallenge } from '../config/oauth2';
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [accessToken, setAccessToken] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing session
checkAuth();
}, []);
async function checkAuth() {
// Try to restore access token from memory or refresh
const storedToken = sessionStorage.getItem('access_token');
const expiresAt = sessionStorage.getItem('expires_at');
if (storedToken && expiresAt && Date.now() < parseInt(expiresAt)) {
setAccessToken(storedToken);
await fetchUserInfo(storedToken);
} else {
// Token expired or doesn't exist, try refresh
await tryRefresh();
}
setLoading(false);
}
async function login() {
// Generate PKCE parameters
const codeVerifier = generateRandomString(64);
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateRandomString(32);
// Store for callback
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('oauth2_state', state);
// Build authorization URL
const params = new URLSearchParams({
client_id: oauth2Config.clientId,
redirect_uri: oauth2Config.redirectUri,
response_type: 'code',
scope: oauth2Config.scopes.join(' '),
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
// Redirect to authorization server
window.location.href = `${oauth2Config.authorizationEndpoint}?${params}`;
}
async function handleCallback(code, state) {
// Validate state
const savedState = sessionStorage.getItem('oauth2_state');
if (state !== savedState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
// Get code verifier
const codeVerifier = sessionStorage.getItem('code_verifier');
if (!codeVerifier) {
throw new Error('Code verifier not found');
}
// Exchange code for tokens
const response = await fetch(oauth2Config.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: oauth2Config.redirectUri,
client_id: oauth2Config.clientId,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
// Store tokens
setAccessToken(tokens.access_token);
sessionStorage.setItem('access_token', tokens.access_token);
sessionStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
// Store refresh token in httpOnly cookie via backend
if (tokens.refresh_token) {
await storeRefreshToken(tokens.refresh_token);
}
// Fetch user info
await fetchUserInfo(tokens.access_token);
// Clean up
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('oauth2_state');
}
async function fetchUserInfo(token) {
const response = await fetch('https://auth.example.com/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const userInfo = await response.json();
setUser(userInfo);
}
}
async function storeRefreshToken(refreshToken) {
// Store refresh token via backend (httpOnly cookie)
await fetch('/api/auth/store-refresh-token', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken }),
});
}
async function tryRefresh() {
try {
// Call backend to refresh using httpOnly cookie
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
const tokens = await response.json();
setAccessToken(tokens.access_token);
sessionStorage.setItem('access_token', tokens.access_token);
sessionStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
await fetchUserInfo(tokens.access_token);
return true;
}
} catch (error) {
console.error('Token refresh failed:', error);
}
return false;
}
async function logout() {
// Revoke tokens
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
// Clear state
setUser(null);
setAccessToken(null);
sessionStorage.clear();
}
const value = {
user,
accessToken,
loading,
login,
logout,
handleCallback,
isAuthenticated: !!accessToken,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// src/components/AuthCallback.js
import React, { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function AuthCallback() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { handleCallback } = useAuth();
const [error, setError] = useState(null);
useEffect(() => {
const code = searchParams.get('code');
const state = searchParams.get('state');
const errorParam = searchParams.get('error');
const errorDescription = searchParams.get('error_description');
if (errorParam) {
setError(errorDescription || errorParam);
return;
}
if (code && state) {
handleCallback(code, state)
.then(() => {
navigate('/dashboard');
})
.catch((err) => {
console.error('Authentication failed:', err);
setError(err.message);
});
} else {
setError('Missing authorization code or state');
}
}, [searchParams, handleCallback, navigate]);
if (error) {
return (
<div className="auth-error">
<h2>Authentication Failed</h2>
<p>{error}</p>
<button onClick={() => navigate('/')}>Return Home</button>
</div>
);
}
return (
<div className="auth-loading">
<h2>Completing authentication...</h2>
<div className="spinner"></div>
</div>
);
}
// src/components/ProtectedRoute.js
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
// src/utils/apiClient.js
import { oauth2Config } from '../config/oauth2';
class ApiClient {
constructor() {
this.baseUrl = 'https://api.example.com';
}
async request(endpoint, options = {}) {
const accessToken = sessionStorage.getItem('access_token');
const expiresAt = sessionStorage.getItem('expires_at');
// Check if token needs refresh
if (!accessToken || Date.now() >= parseInt(expiresAt)) {
await this.refreshToken();
}
const token = sessionStorage.getItem('access_token');
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
// Token invalid, try refresh once
await this.refreshToken();
const newToken = sessionStorage.getItem('access_token');
// Retry request
const retryResponse = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`,
'Content-Type': 'application/json',
},
});
if (!retryResponse.ok) {
throw new Error('API request failed after token refresh');
}
return retryResponse.json();
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
async refreshToken() {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
// Refresh failed, redirect to login
window.location.href = '/login';
throw new Error('Token refresh failed');
}
const tokens = await response.json();
sessionStorage.setItem('access_token', tokens.access_token);
sessionStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
}
async get(endpoint) {
return this.request(endpoint);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE',
});
}
}
export const apiClient = new ApiClient();
Machine-to-machine authentication for services and APIs
// Service-to-service authentication
class OAuth2Client {
constructor(config) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.tokenEndpoint = config.tokenEndpoint;
this.audience = config.audience;
this.scopes = config.scopes || [];
this.accessToken = null;
this.tokenExpiry = null;
}
async getAccessToken() {
// Return cached token if still valid
if (this.accessToken && Date.now() < this.tokenExpiry - 60000) {
return this.accessToken;
}
// Request new token
const response = await fetch(this.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
audience: this.audience,
scope: this.scopes.join(' '),
}),
});
if (!response.ok) {
throw new Error('Failed to obtain access token');
}
const data = await response.json();
// Cache token
this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + (data.expires_in * 1000);
return this.accessToken;
}
async callApi(endpoint, options = {}) {
const token = await this.getAccessToken();
const response = await fetch(endpoint, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
if (response.status === 401) {
// Token might be invalid, force refresh and retry
this.accessToken = null;
const newToken = await this.getAccessToken();
const retryResponse = await fetch(endpoint, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`,
},
});
return retryResponse;
}
return response;
}
}
// Usage
const oauth2Client = new OAuth2Client({
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
tokenEndpoint: 'https://auth.example.com/oauth/token',
audience: 'https://api.example.com',
scopes: ['read:data', 'write:data'],
});
// Make API calls
async function fetchData() {
const response = await oauth2Client.callApi('https://api.example.com/data');
return response.json();
}
# client_credentials_oauth2.py
import requests
import time
from datetime import datetime, timedelta
class OAuth2Client:
def __init__(self, client_id, client_secret, token_endpoint, audience=None, scopes=None):
self.client_id = client_id
self.client_secret = client_secret
self.token_endpoint = token_endpoint
self.audience = audience
self.scopes = scopes or []
self.access_token = None
self.token_expiry = None
def get_access_token(self):
"""Get cached token or request new one"""
# Return cached token if valid
if self.access_token and datetime.now() < self.token_expiry - timedelta(minutes=1):
return self.access_token
# Request new token
data = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret,
}
if self.audience:
data['audience'] = self.audience
if self.scopes:
data['scope'] = ' '.join(self.scopes)
response = requests.post(
self.token_endpoint,
data=data,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
if not response.ok:
raise Exception(f'Failed to obtain access token: {response.text}')
token_data = response.json()
# Cache token
self.access_token = token_data['access_token']
self.token_expiry = datetime.now() + timedelta(seconds=token_data['expires_in'])
return self.access_token
def call_api(self, url, method='GET', **kwargs):
"""Make authenticated API request"""
token = self.get_access_token()
headers = kwargs.pop('headers', {})
headers['Authorization'] = f'Bearer {token}'
response = requests.request(method, url, headers=headers, **kwargs)
# Handle token expiration
if response.status_code == 401:
# Force token refresh and retry
self.access_token = None
token = self.get_access_token()
headers['Authorization'] = f'Bearer {token}'
response = requests.request(method, url, headers=headers, **kwargs)
return response
# Usage
client = OAuth2Client(
client_id='your_client_id',
client_secret='your_client_secret',
token_endpoint='https://auth.example.com/oauth/token',
audience='https://api.example.com',
scopes=['read:data', 'write:data']
)
# Make API requests
response = client.call_api('https://api.example.com/data')
data = response.json()
Authentication with identity verification
// id-token-validator.js
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
class IDTokenValidator {
constructor(config) {
this.issuer = config.issuer;
this.audience = config.audience;
this.jwksUri = config.jwksUri;
// JWKS client for fetching public keys
this.client = jwksClient({
jwksUri: this.jwksUri,
cache: true,
cacheMaxAge: 86400000, // 24 hours
});
}
async getSigningKey(kid) {
const key = await this.client.getSigningKey(kid);
return key.getPublicKey();
}
async validate(idToken) {
try {
// Decode token header to get key ID
const decoded = jwt.decode(idToken, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
// Get public key
const publicKey = await this.getSigningKey(decoded.header.kid);
// Verify and decode token
const payload = jwt.verify(idToken, publicKey, {
issuer: this.issuer,
audience: this.audience,
algorithms: ['RS256', 'ES256'],
});
// Additional validations
this.validateClaims(payload);
return payload;
} catch (error) {
throw new Error(`ID token validation failed: ${error.message}`);
}
}
validateClaims(payload) {
// Check required claims
if (!payload.sub) {
throw new Error('Missing sub claim');
}
if (!payload.iat || !payload.exp) {
throw new Error('Missing iat or exp claim');
}
// Check token is not expired (already checked by jwt.verify, but double-check)
const now = Math.floor(Date.now() / 1000);
if (payload.exp < now) {
throw new Error('Token expired');
}
// Check token was not issued in the future
if (payload.iat > now + 60) {
throw new Error('Token issued in the future');
}
// Optional: Check nonce if provided
// if (payload.nonce && payload.nonce !== expectedNonce) {
// throw new Error('Nonce mismatch');
// }
return true;
}
}
// Usage
const validator = new IDTokenValidator({
issuer: 'https://auth.example.com',
audience: 'your_client_id',
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
});
async function validateIDToken(idToken) {
try {
const payload = await validator.validate(idToken);
console.log('User ID:', payload.sub);
console.log('Email:', payload.email);
console.log('Name:', payload.name);
return payload;
} catch (error) {
console.error('ID token validation failed:', error);
throw error;
}
}
// userinfo-client.js
class UserInfoClient {
constructor(userInfoEndpoint) {
this.endpoint = userInfoEndpoint;
}
async getUserInfo(accessToken) {
const response = await fetch(this.endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`UserInfo request failed: ${response.status}`);
}
return response.json();
}
async getUserInfoWithValidation(accessToken, idTokenPayload) {
const userInfo = await this.getUserInfo(accessToken);
// Validate that sub matches ID token
if (userInfo.sub !== idTokenPayload.sub) {
throw new Error('UserInfo sub does not match ID token sub');
}
return userInfo;
}
}
// Usage
const userInfoClient = new UserInfoClient('https://auth.example.com/oauth/userinfo');
async function getCompleteUserProfile(accessToken, idToken, validator) {
// Validate ID token
const idTokenPayload = await validator.validate(idToken);
// Fetch additional user info
const userInfo = await userInfoClient.getUserInfoWithValidation(accessToken, idTokenPayload);
// Combine information
return {
userId: userInfo.sub,
email: userInfo.email,
emailVerified: userInfo.email_verified,
name: userInfo.name,
givenName: userInfo.given_name,
familyName: userInfo.family_name,
picture: userInfo.picture,
locale: userInfo.locale,
// Any custom claims
...userInfo,
};
}
// Token storage strategies for web applications
// ❌ BAD: localStorage (vulnerable to XSS)
localStorage.setItem('access_token', token); // DON'T DO THIS
// ❌ BAD: sessionStorage (also vulnerable to XSS)
sessionStorage.setItem('access_token', token); // DON'T DO THIS
// ✅ GOOD: In-memory storage (cleared on page refresh)
class TokenStore {
constructor() {
this.accessToken = null;
this.refreshToken = null;
}
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
// Refresh token stored in httpOnly cookie via backend
// this.refreshToken = refreshToken; // Don't store in memory
}
getAccessToken() {
return this.accessToken;
}
clear() {
this.accessToken = null;
this.refreshToken = null;
}
}
// ✅ BEST: Backend For Frontend (BFF) Pattern
// - All tokens stored on backend
// - Session cookie identifies user
// - No tokens exposed to browser
// Secure token storage for React Native
import * as SecureStore from 'expo-secure-store';
// or
// import EncryptedStorage from 'react-native-encrypted-storage';
class MobileTokenStore {
async saveTokens(accessToken, refreshToken) {
try {
await SecureStore.setItemAsync('access_token', accessToken);
await SecureStore.setItemAsync('refresh_token', refreshToken);
await SecureStore.setItemAsync('token_expiry', Date.now().toString());
} catch (error) {
console.error('Failed to store tokens:', error);
throw error;
}
}
async getAccessToken() {
try {
return await SecureStore.getItemAsync('access_token');
} catch (error) {
console.error('Failed to retrieve access token:', error);
return null;
}
}
async getRefreshToken() {
try {
return await SecureStore.getItemAsync('refresh_token');
} catch (error) {
console.error('Failed to retrieve refresh token:', error);
return null;
}
}
async clear() {
try {
await SecureStore.deleteItemAsync('access_token');
await SecureStore.deleteItemAsync('refresh_token');
await SecureStore.deleteItemAsync('token_expiry');
} catch (error) {
console.error('Failed to clear tokens:', error);
}
}
async isTokenExpired() {
try {
const expiry = await SecureStore.getItemAsync('token_expiry');
if (!expiry) return true;
return Date.now() > parseInt(expiry);
} catch (error) {
return true;
}
}
}
export const tokenStore = new MobileTokenStore();
// Proactive token refresh before expiration
class TokenRefreshManager {
constructor(refreshCallback, expiresIn) {
this.refreshCallback = refreshCallback;
this.expiresIn = expiresIn;
this.refreshTimer = null;
}
scheduleRefresh() {
// Refresh 5 minutes before expiration
const refreshIn = (this.expiresIn - 300) * 1000;
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
this.refreshTimer = setTimeout(async () => {
try {
await this.refreshCallback();
} catch (error) {
console.error('Token refresh failed:', error);
// Optionally: Redirect to login
}
}, refreshIn);
}
cancelRefresh() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
}
}
// Usage
const refreshManager = new TokenRefreshManager(
async () => {
// Refresh token logic
const newTokens = await refreshTokens();
setAccessToken(newTokens.access_token);
refreshManager.expiresIn = newTokens.expires_in;
refreshManager.scheduleRefresh();
},
3600 // Initial expires_in
);
// Start automatic refresh
refreshManager.scheduleRefresh();
// Refresh token only when access token is expired
class ReactiveTokenManager {
constructor() {
this.accessToken = null;
this.expiresAt = null;
this.refreshPromise = null; // Prevent concurrent refreshes
}
async getValidToken() {
// Check if token is still valid
if (this.accessToken && Date.now() < this.expiresAt - 60000) {
return this.accessToken;
}
// Token expired, refresh it
if (!this.refreshPromise) {
this.refreshPromise = this.refreshToken()
.finally(() => {
this.refreshPromise = null;
});
}
await this.refreshPromise;
return this.accessToken;
}
async refreshToken() {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // Send refresh token cookie
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
this.accessToken = tokens.access_token;
this.expiresAt = Date.now() + (tokens.expires_in * 1000);
return this.accessToken;
}
clearTokens() {
this.accessToken = null;
this.expiresAt = null;
this.refreshPromise = null;
}
}
// Usage in API client
const tokenManager = new ReactiveTokenManager();
async function makeApiRequest(endpoint) {
const token = await tokenManager.getValidToken();
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
return response;
}
// Implementing token revocation
async function revokeToken(token, tokenTypeHint = 'access_token') {
const response = await fetch('https://auth.example.com/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
token: token,
token_type_hint: tokenTypeHint, // 'access_token' or 'refresh_token'
client_id: oauth2Config.clientId,
client_secret: oauth2Config.clientSecret, // If confidential client
}),
});
if (!response.ok) {
throw new Error('Token revocation failed');
}
return true;
}
// Logout with token revocation
async function logout() {
try {
// Revoke access token
if (accessToken) {
await revokeToken(accessToken, 'access_token');
}
// Revoke refresh token
if (refreshToken) {
await revokeToken(refreshToken, 'refresh_token');
}
// Clear local state
clearTokens();
// Optional: Redirect to logout endpoint for session cleanup
window.location.href = 'https://auth.example.com/logout?redirect_uri=' +
encodeURIComponent(window.location.origin);
} catch (error) {
console.error('Logout failed:', error);
// Clear tokens anyway
clearTokens();
}
}
Implementing OAuth2 provider functionality
// Express.js authorization endpoint
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
router.get('/oauth/authorize', async (req, res) => {
const {
client_id,
redirect_uri,
response_type,
scope,
state,
code_challenge,
code_challenge_method,
} = req.query;
// Validate required parameters
if (!client_id || !redirect_uri || !response_type) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'Missing required parameters',
});
}
// Validate client_id and redirect_uri
const client = await getClient(client_id);
if (!client) {
return res.status(400).json({
error: 'invalid_client',
error_description: 'Unknown client',
});
}
if (!client.redirect_uris.includes(redirect_uri)) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'Invalid redirect_uri',
});
}
// Validate response_type
if (response_type !== 'code') {
return redirectWithError(redirect_uri, state, 'unsupported_response_type',
'Only authorization code flow is supported');
}
// Validate PKCE parameters
if (code_challenge) {
if (!code_challenge_method) {
return redirectWithError(redirect_uri, state, 'invalid_request',
'code_challenge_method is required when using PKCE');
}
if (code_challenge_method !== 'S256' && code_challenge_method !== 'plain') {
return redirectWithError(redirect_uri, state, 'invalid_request',
'Unsupported code_challenge_method');
}
}
// Check if user is authenticated
if (!req.session.userId) {
// Store authorization request and redirect to login
req.session.authRequest = {
client_id,
redirect_uri,
scope,
state,
code_challenge,
code_challenge_method,
};
return res.redirect('/login?continue=' + encodeURIComponent(req.originalUrl));
}
// User is authenticated, show consent screen
res.render('consent', {
client: client,
scopes: scope ? scope.split(' ') : [],
state: state,
});
});
// Consent endpoint
router.post('/oauth/consent', async (req, res) => {
const { approved } = req.body;
const authRequest = req.session.authRequest;
if (!authRequest) {
return res.status(400).send('No pending authorization request');
}
if (approved !== 'true') {
// User denied consent
return redirectWithError(
authRequest.redirect_uri,
authRequest.state,
'access_denied',
'User denied consent'
);
}
// Generate authorization code
const authorizationCode = crypto.randomBytes(32).toString('hex');
// Store authorization code with metadata
await storeAuthorizationCode(authorizationCode, {
client_id: authRequest.client_id,
redirect_uri: authRequest.redirect_uri,
user_id: req.session.userId,
scope: authRequest.scope,
code_challenge: authRequest.code_challenge,
code_challenge_method: authRequest.code_challenge_method,
expires_at: Date.now() + 600000, // 10 minutes
});
// Clear auth request from session
delete req.session.authRequest;
// Redirect to client with authorization code
const redirectUrl = new URL(authRequest.redirect_uri);
redirectUrl.searchParams.set('code', authorizationCode);
if (authRequest.state) {
redirectUrl.searchParams.set('state', authRequest.state);
}
res.redirect(redirectUrl.toString());
});
function redirectWithError(redirectUri, state, error, errorDescription) {
const url = new URL(redirectUri);
url.searchParams.set('error', error);
url.searchParams.set('error_description', errorDescription);
if (state) {
url.searchParams.set('state', state);
}
return res.redirect(url.toString());
}
// Token endpoint implementation
router.post('/oauth/token', async (req, res) => {
const { grant_type } = req.body;
try {
let tokens;
switch (grant_type) {
case 'authorization_code':
tokens = await handleAuthorizationCodeGrant(req.body);
break;
case 'refresh_token':
tokens = await handleRefreshTokenGrant(req.body);
break;
case 'client_credentials':
tokens = await handleClientCredentialsGrant(req.body);
break;
default:
return res.status(400).json({
error: 'unsupported_grant_type',
error_description: `Grant type '${grant_type}' is not supported`,
});
}
res.json(tokens);
} catch (error) {
console.error('Token endpoint error:', error);
res.status(400).json({
error: error.code || 'invalid_request',
error_description: error.message,
});
}
});
async function handleAuthorizationCodeGrant(params) {
const {
code,
redirect_uri,
client_id,
client_secret,
code_verifier,
} = params;
// Validate required parameters
if (!code || !redirect_uri || !client_id) {
throw { code: 'invalid_request', message: 'Missing required parameters' };
}
// Retrieve authorization code
const authCode = await getAuthorizationCode(code);
if (!authCode) {
throw { code: 'invalid_grant', message: 'Invalid authorization code' };
}
// Check expiration
if (Date.now() > authCode.expires_at) {
await deleteAuthorizationCode(code);
throw { code: 'invalid_grant', message: 'Authorization code expired' };
}
// Validate client
if (authCode.client_id !== client_id) {
throw { code: 'invalid_grant', message: 'Client mismatch' };
}
// Validate redirect URI
if (authCode.redirect_uri !== redirect_uri) {
throw { code: 'invalid_grant', message: 'Redirect URI mismatch' };
}
// Authenticate client
const client = await getClient(client_id);
if (client.client_type === 'confidential') {
// Confidential client must provide client_secret
if (client.client_secret !== client_secret) {
throw { code: 'invalid_client', message: 'Invalid client credentials' };
}
}
// Validate PKCE if used
if (authCode.code_challenge) {
if (!code_verifier) {
throw { code: 'invalid_request', message: 'code_verifier is required' };
}
const isValid = validatePKCE(
code_verifier,
authCode.code_challenge,
authCode.code_challenge_method
);
if (!isValid) {
throw { code: 'invalid_grant', message: 'Invalid code_verifier' };
}
}
// Delete authorization code (single use)
await deleteAuthorizationCode(code);
// Generate tokens
const accessToken = await generateAccessToken({
user_id: authCode.user_id,
client_id: client_id,
scope: authCode.scope,
});
const refreshToken = await generateRefreshToken({
user_id: authCode.user_id,
client_id: client_id,
scope: authCode.scope,
});
// Generate ID token if openid scope was requested
let idToken = null;
if (authCode.scope && authCode.scope.includes('openid')) {
idToken = await generateIDToken({
user_id: authCode.user_id,
client_id: client_id,
scope: authCode.scope,
});
}
const response = {
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
};
if (idToken) {
response.id_token = idToken;
}
return response;
}
function validatePKCE(codeVerifier, codeChallenge, method) {
if (method === 'plain') {
return codeVerifier === codeChallenge;
}
if (method === 'S256') {
const hash = crypto.createHash('sha256').update(codeVerifier).digest();
const computed = base64UrlEncode(hash);
return computed === codeChallenge;
}
return false;
}
async function handleRefreshTokenGrant(params) {
const { refresh_token, client_id, client_secret, scope } = params;
// Validate refresh token
const storedToken = await getRefreshToken(refresh_token);
if (!storedToken) {
throw { code: 'invalid_grant', message: 'Invalid refresh token' };
}
// Check if revoked
if (storedToken.revoked) {
throw { code: 'invalid_grant', message: 'Refresh token has been revoked' };
}
// Validate client
if (storedToken.client_id !== client_id) {
throw { code: 'invalid_grant', message: 'Client mismatch' };
}
const client = await getClient(client_id);
if (client.client_type === 'confidential' && client.client_secret !== client_secret) {
throw { code: 'invalid_client', mes
Azure PostgreSQL 无密码身份验证配置指南:Entra ID 迁移与访问管理
34,800 周安装