505 lines
13 KiB
JavaScript
505 lines
13 KiB
JavaScript
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 { oauthAuthLimiter, oauthTokenLimiter } = require('../middleware/rateLimit');
|
||
|
||
const router = express.Router();
|
||
|
||
// 验证重定向URI的通用函数
|
||
const isValidRedirectUri = (storedUris, requestedUri) => {
|
||
try {
|
||
const requestedUrl = new URL(requestedUri);
|
||
|
||
for (const storedUri of storedUris) {
|
||
const storedUrl = new URL(storedUri);
|
||
|
||
// 检查协议是否匹配
|
||
if (requestedUrl.protocol !== storedUrl.protocol) {
|
||
continue;
|
||
}
|
||
|
||
// 获取文件名(路径的最后一部分)
|
||
const requestedFilename = requestedUrl.pathname.split('/').pop();
|
||
const storedFilename = storedUrl.pathname.split('/').pop();
|
||
|
||
// 检查文件名是否匹配,忽略路径前缀
|
||
if (requestedFilename === storedFilename) {
|
||
return true;
|
||
}
|
||
|
||
// 如果文件名不匹配,检查完整路径是否匹配
|
||
if (requestedUrl.pathname === storedUrl.pathname) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// 验证中间件
|
||
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', oauthAuthLimiter, 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 (!isValidRedirectUri(client.redirect_uris, redirect_uri)) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '无效的重定向URI',
|
||
debug: {
|
||
requested: redirect_uri,
|
||
allowed: client.redirect_uris
|
||
}
|
||
});
|
||
}
|
||
|
||
// 获取用户信息(通过JWT中间件已经验证)
|
||
const user = await User.findByUsername(req.user.username);
|
||
if (!user) {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '用户不存在'
|
||
});
|
||
}
|
||
|
||
// 返回授权信息,让前端显示授权页面
|
||
const scopes = scope ? scope.split(' ') : ['read', 'write'];
|
||
|
||
res.json({
|
||
success: true,
|
||
message: '授权信息获取成功',
|
||
data: {
|
||
client: {
|
||
id: client.client_id,
|
||
name: client.client_name,
|
||
description: client.description
|
||
},
|
||
scopes: scopes,
|
||
state: state,
|
||
redirect_uri: redirect_uri
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('授权信息获取失败:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: '服务器内部错误'
|
||
});
|
||
}
|
||
});
|
||
|
||
// 2. 用户同意/拒绝授权端点
|
||
router.post('/authorize/consent', oauthAuthLimiter, authenticateToken, async (req, res) => {
|
||
try {
|
||
const {
|
||
client_id,
|
||
redirect_uri,
|
||
scope,
|
||
state,
|
||
approved
|
||
} = req.body;
|
||
|
||
// 验证必需参数
|
||
if (!client_id || !redirect_uri || approved === undefined) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '缺少必需参数'
|
||
});
|
||
}
|
||
|
||
// 验证客户端
|
||
const client = await OAuthClient.findByClientId(client_id);
|
||
if (!client) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '无效的客户端ID'
|
||
});
|
||
}
|
||
|
||
// 验证重定向URI
|
||
if (!isValidRedirectUri(client.redirect_uris, redirect_uri)) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '无效的重定向URI',
|
||
debug: {
|
||
requested: redirect_uri,
|
||
allowed: client.redirect_uris
|
||
}
|
||
});
|
||
}
|
||
|
||
// 获取用户信息
|
||
const user = await User.findByUsername(req.user.username);
|
||
if (!user) {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '用户不存在'
|
||
});
|
||
}
|
||
|
||
if (!approved) {
|
||
// 用户拒绝授权
|
||
const errorUrl = new URL(redirect_uri);
|
||
errorUrl.searchParams.set('error', 'access_denied');
|
||
errorUrl.searchParams.set('error_description', '用户拒绝授权');
|
||
if (state) {
|
||
errorUrl.searchParams.set('state', state);
|
||
}
|
||
|
||
return res.json({
|
||
success: true,
|
||
message: '用户拒绝授权',
|
||
data: {
|
||
redirect_url: errorUrl.toString()
|
||
}
|
||
});
|
||
}
|
||
|
||
// 用户同意授权,生成授权码
|
||
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);
|
||
}
|
||
|
||
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: '服务器内部错误'
|
||
});
|
||
}
|
||
});
|
||
|
||
// 3. 令牌端点 - 交换授权码获取访问令牌
|
||
router.post('/token', oauthTokenLimiter, [
|
||
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: '服务器内部错误'
|
||
});
|
||
}
|
||
});
|
||
|
||
// 4. 撤销令牌端点
|
||
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: '服务器内部错误'
|
||
});
|
||
}
|
||
});
|
||
|
||
// 5. 用户信息端点
|
||
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: '服务器内部错误'
|
||
});
|
||
}
|
||
});
|
||
|
||
// 6. 令牌信息端点
|
||
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; |