Files
pdnode-account/routes/oauth.js
2025-07-29 15:36:25 -07:00

375 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;