This commit is contained in:
2025-07-29 15:36:25 -07:00
commit 0c481c7a0e
29 changed files with 6682 additions and 0 deletions

375
routes/oauth.js Normal file
View File

@ -0,0 +1,375 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const OAuthClient = require('../models/OAuthClient');
const OAuthToken = require('../models/OAuthToken');
const User = require('../models/User');
const { authenticateToken } = require('../middleware/auth');
const { authenticateOAuthToken, requireScope } = require('../middleware/oauth');
const router = express.Router();
// 验证中间件
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: '输入验证失败',
errors: errors.array().map(error => ({
field: error.path,
message: error.msg
}))
});
}
next();
};
// 1. 授权端点 - 用户授权第三方应用
router.get('/authorize', authenticateToken, async (req, res) => {
try {
const {
response_type,
client_id,
redirect_uri,
scope,
state
} = req.query;
// 验证必需参数
if (!response_type || !client_id || !redirect_uri) {
return res.status(400).json({
success: false,
message: '缺少必需参数'
});
}
// 验证response_type
if (response_type !== 'code') {
return res.status(400).json({
success: false,
message: '不支持的response_type'
});
}
// 验证客户端
const client = await OAuthClient.findByClientId(client_id);
if (!client) {
return res.status(400).json({
success: false,
message: '无效的客户端ID'
});
}
// 验证重定向URI
if (!client.redirect_uris.includes(redirect_uri)) {
return res.status(400).json({
success: false,
message: '无效的重定向URI'
});
}
// 获取用户信息通过JWT中间件已经验证
const user = await User.findByUsername(req.user.username);
if (!user) {
return res.status(401).json({
success: false,
message: '用户不存在'
});
}
// 生成授权码
const authCode = OAuthToken.generateAuthCode();
const scopes = scope ? scope.split(' ') : ['read', 'write'];
await OAuthToken.createAuthCode({
code: authCode,
clientId: client_id,
userId: user.id,
redirectUri: redirect_uri,
scopes: scopes
});
// 构建重定向URL
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', authCode);
if (state) {
redirectUrl.searchParams.set('state', state);
}
// 对于API测试直接返回授权码
// 对于生产环境应该重定向到redirectUrl
res.json({
success: true,
message: '授权成功',
data: {
redirect_url: redirectUrl.toString(),
code: authCode,
state: state
}
});
} catch (error) {
console.error('授权失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
});
// 2. 令牌端点 - 交换授权码获取访问令牌
router.post('/token', [
body('grant_type').notEmpty().withMessage('grant_type不能为空'),
body('client_id').notEmpty().withMessage('client_id不能为空'),
body('client_secret').notEmpty().withMessage('client_secret不能为空'),
handleValidationErrors
], async (req, res) => {
try {
const {
grant_type,
client_id,
client_secret,
code,
redirect_uri,
refresh_token,
code_verifier // PKCE支持
} = req.body;
// 验证客户端
const isValidClient = await OAuthClient.validateClient(client_id, client_secret);
if (!isValidClient) {
return res.status(401).json({
success: false,
message: '无效的客户端凭据'
});
}
if (grant_type === 'authorization_code') {
// 授权码流程
if (!code || !redirect_uri) {
return res.status(400).json({
success: false,
message: '缺少code或redirect_uri'
});
}
// 验证授权码
const authCode = await OAuthToken.validateAuthCode(code, client_id, redirect_uri);
if (!authCode) {
return res.status(400).json({
success: false,
message: '无效的授权码'
});
}
// 验证重定向URI
const isValidRedirectUri = await OAuthClient.validateRedirectUri(client_id, redirect_uri);
if (!isValidRedirectUri) {
return res.status(400).json({
success: false,
message: '无效的重定向URI'
});
}
// 生成访问令牌和刷新令牌
const accessToken = OAuthToken.generateAccessToken();
const refreshToken = OAuthToken.generateRefreshToken();
// 创建访问令牌
const accessTokenData = await OAuthToken.createAccessToken({
token: accessToken,
clientId: client_id,
userId: authCode.user_id,
scopes: authCode.scopes
});
// 创建刷新令牌
await OAuthToken.createRefreshToken({
token: refreshToken,
accessTokenId: accessTokenData.id,
clientId: client_id,
userId: authCode.user_id
});
// 删除已使用的授权码
await OAuthToken.deleteAuthCode(code);
res.json({
success: true,
data: {
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600, // 1小时
refresh_token: refreshToken,
scope: authCode.scopes.join(' ')
}
});
} else if (grant_type === 'refresh_token') {
// 刷新令牌流程
if (!refresh_token) {
return res.status(400).json({
success: false,
message: '缺少refresh_token'
});
}
// 验证刷新令牌
const refreshTokenData = await OAuthToken.validateRefreshToken(refresh_token, client_id);
if (!refreshTokenData) {
return res.status(400).json({
success: false,
message: '无效的刷新令牌'
});
}
// 生成新的访问令牌
const newAccessToken = OAuthToken.generateAccessToken();
const newRefreshToken = OAuthToken.generateRefreshToken();
// 创建新的访问令牌
const newAccessTokenData = await OAuthToken.createAccessToken({
token: newAccessToken,
clientId: client_id,
userId: refreshTokenData.user_id,
scopes: refreshTokenData.scopes
});
// 创建新的刷新令牌
await OAuthToken.createRefreshToken({
token: newRefreshToken,
accessTokenId: newAccessTokenData.id,
clientId: client_id,
userId: refreshTokenData.user_id
});
// 撤销旧的刷新令牌
await OAuthToken.revokeRefreshToken(refresh_token);
res.json({
success: true,
data: {
access_token: newAccessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: newRefreshToken,
scope: refreshTokenData.scopes.join(' ')
}
});
} else {
return res.status(400).json({
success: false,
message: '不支持的授权类型'
});
}
} catch (error) {
console.error('令牌交换失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
});
// 3. 撤销令牌端点
router.post('/revoke', [
body('token').notEmpty().withMessage('token不能为空'),
body('client_id').notEmpty().withMessage('client_id不能为空'),
body('client_secret').notEmpty().withMessage('client_secret不能为空'),
handleValidationErrors
], async (req, res) => {
try {
const { token, client_id, client_secret } = req.body;
// 验证客户端
const isValidClient = await OAuthClient.validateClient(client_id, client_secret);
if (!isValidClient) {
return res.status(401).json({
success: false,
message: '无效的客户端凭据'
});
}
// 撤销令牌
const revoked = await OAuthToken.revokeAccessToken(token);
if (!revoked) {
await OAuthToken.revokeRefreshToken(token);
}
res.json({
success: true,
message: '令牌已撤销'
});
} catch (error) {
console.error('撤销令牌失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
});
// 4. 用户信息端点
router.get('/userinfo', authenticateOAuthToken, requireScope('read'), async (req, res) => {
try {
res.json({
success: true,
data: {
user_id: req.oauth.userId,
username: req.oauth.username,
email: req.oauth.email,
scopes: req.oauth.scopes
}
});
} catch (error) {
console.error('获取用户信息失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
});
// 5. 令牌信息端点
router.get('/tokeninfo', async (req, res) => {
try {
const { access_token } = req.query;
if (!access_token) {
return res.status(400).json({
success: false,
message: '缺少access_token参数'
});
}
const tokenData = await OAuthToken.validateAccessToken(access_token);
if (!tokenData) {
return res.status(400).json({
success: false,
message: '无效的访问令牌'
});
}
res.json({
success: true,
data: {
user_id: tokenData.user_id,
client_id: tokenData.client_id,
scopes: tokenData.scopes,
expires_in: Math.floor((new Date(tokenData.expires_at) - new Date()) / 1000)
}
});
} catch (error) {
console.error('获取令牌信息失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
});
module.exports = router;