Files
d1kt/server/routes/oauth2.controller.ts

356 lines
11 KiB
TypeScript
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.
import { Request, Response } from 'express';
import { OAuth2Client, OAuth2AuthorizationCode, OAuth2AccessToken } from '../models/oauth2';
import { User } from '../models/User';
import { generateRandomString } from '../utils/crypto';
import { Session } from 'express-session';
import fetch from 'node-fetch';
import jwt from 'jsonwebtoken';
import { config } from '../config';
// JWT Payload 类型定义
interface UserPayload {
_id: string;
username: string;
fullname: string;
email: string;
isAdmin: boolean;
}
// 添加 session 接口声明
interface CustomSession extends Session {
userId?: string;
isAuthenticated?: boolean;
}
// 扩展 Request 类型
interface CustomRequest extends Request {
session: CustomSession;
}
export class OAuth2Controller {
// 授权端点
async authorize(req: CustomRequest, res: Response) {
try {
// 调试日志:输出请求信息
console.log('--- OAuth2 authorize 调试信息 ---');
console.log('req.headers:', req.headers);
console.log('req.originalUrl:', req.originalUrl);
const { client_id, redirect_uri, scope, response_type, state } = req.query;
// 统一使用 JWT 认证,从多个来源获取 token
let token: string | undefined;
let currentUser: UserPayload | null = null;
// 1. 检查 Authorization 头
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
// 2. 检查 cookie如果存在
if (!token && req.headers.cookie) {
const cookies: Record<string, string> = {};
req.headers.cookie.split(';').forEach(cookie => {
const parts = cookie.trim().split('=');
if (parts.length === 2) {
cookies[parts[0]] = parts[1];
}
});
// 尝试从 cookie 中获取 token
if (cookies.token) {
token = cookies.token;
}
}
// 3. 验证 token 并获取用户信息
if (token) {
try {
const decoded = jwt.verify(token, config.JWT_SECRET);
if (typeof decoded === 'object' && decoded !== null && '_id' in decoded) {
// 伪造session让后续逻辑认为已登录
req.session.userId = decoded._id as string;
req.session.isAuthenticated = true;
currentUser = decoded as UserPayload;
console.log('JWT验证成功用户信息:', {
userId: currentUser._id,
username: currentUser.username
});
}
} catch (error) {
console.log('JWT验证失败:', error);
}
}
// 如果用户未登录,重定向到登录页面
if (!currentUser) {
console.log('用户未登录,重定向到登录页');
const redirectPath = '/api' + req.originalUrl;
const loginUrl = `/login?redirect=${encodeURIComponent(redirectPath)}`;
return res.redirect(loginUrl);
}
// 用户已登录继续OAuth2授权流程
console.log('用户已登录继续OAuth2授权流程');
// 从数据库中获取完整用户信息
const user = await User.findById(currentUser._id);
if (!user) {
console.log('数据库中未找到用户:', currentUser._id);
return res.status(401).json({ error: 'user_not_found' });
}
// 验证客户端
const client = await OAuth2Client.findOne({ clientId: client_id });
if (!client) {
return res.status(400).json({ error: 'invalid_client' });
}
console.log('OAuth2 authorize start:', {
user: user.username,
clientId: client_id
});
// 查找现有的用户关联
const existingLink = await OAuth2Client.findOne({
clientId: client_id,
'linkedUsers.username': user.username
});
console.log('OAuth2 link check:', {
username: user.username,
exists: !!existingLink,
linkDetails: existingLink
});
// 只在没有现有关联时创建新的关联
if (!existingLink) {
console.log('Creating new OAuth link');
await OAuth2Client.updateOne(
{ clientId: client_id },
{
$addToSet: {
linkedUsers: {
userId: user._id,
username: user.username,
email: user.email
}
}
}
);
} else {
console.log('Using existing OAuth link - proceeding with authorization');
}
// 生成授权码
const code = generateRandomString(32);
const authCode = new OAuth2AuthorizationCode({
code,
clientId: client_id,
userId: user._id,
redirectUri: redirect_uri,
scope: (typeof scope === 'string' ? scope.split(' ') : []) || [],
expiresAt: new Date(Date.now() + 10 * 60 * 1000)
});
await authCode.save();
console.log('Generated auth code:', { code, userId: user._id });
// 重定向回客户端
if (!redirect_uri || typeof redirect_uri !== 'string') {
return res.status(400).json({ error: 'invalid_redirect_uri' });
}
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', code);
redirectUrl.searchParams.set('state', state?.toString() || '');
console.log('Redirecting to:', redirectUrl.toString());
return res.redirect(redirectUrl.toString());
} catch (error: any) {
console.error('OAuth2 authorize error:', error);
throw error;
}
}
// 令牌端点
async token(req: Request, res: Response) {
try {
console.log('=== OAuth2 Token Debug Info ===');
console.log('Token request body:', req.body);
const { grant_type, code, client_id, client_secret, redirect_uri } = req.body;
// 验证客户端
const client = await OAuth2Client.findOne({
clientId: client_id
});
if (!client) {
console.log('Client not found:', client_id);
return res.status(401).json({ error: 'invalid_client' });
}
if (grant_type === 'authorization_code') {
// 查找授权码
const authCode = await OAuth2AuthorizationCode.findOne({ code });
console.log('Found auth code:', authCode);
if (!authCode || authCode.expiresAt < new Date()) {
console.log('Invalid or expired auth code');
return res.status(400).json({ error: 'invalid_grant' });
}
// 查找用户
const user = await User.findById(authCode.userId);
// 生成访问令牌
const accessToken = generateRandomString(32);
const token = new OAuth2AccessToken({
accessToken,
clientId: client_id,
userId: authCode.userId,
scope: authCode.scope,
expiresAt: new Date(Date.now() + 60 * 60 * 1000)
});
await token.save();
console.log('Generated access token:', accessToken);
// 生成 id_token只要 scope 里有 openid
let id_token;
if (authCode.scope.includes('openid')) {
const jwt = require('jsonwebtoken');
id_token = jwt.sign(
{
sub: user._id.toString(),
name: user.username,
fullname: user.fullname,
email: user.email,
// 可根据需要添加其它 claim
},
process.env.OPENID_SESSION_SECRET || 'your-secret-keyq', // 用于签名的密钥
{
algorithm: 'HS256',
expiresIn: '1h',
issuer: process.env.OPENID_ISSUER || 'https://d1kt.cn',
audience: client_id,
}
);
}
// 返回令牌响应
return res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: authCode.scope.join(' '),
...(id_token ? { id_token } : {}),
});
}
return res.status(400).json({ error: 'unsupported_grant_type' });
} catch (error: any) {
console.error('OAuth2 Token Error:', {
message: error.message,
stack: error.stack,
body: req.body
});
return res.status(500).json({
error: 'server_error',
error_description: error.message
});
}
}
// 用户信息端点
async userinfo(req: Request, res: Response) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'invalid_token' });
}
const accessToken = authHeader.substring(7);
try {
const token = await OAuth2AccessToken.findOne({
accessToken,
expiresAt: { $gt: new Date() }
});
if (!token) {
return res.status(401).json({ error: 'invalid_token' });
}
// 查找已关联的用户
const linkedUser = await OAuth2Client.findOne({
clientId: token.clientId,
'linkedUsers.userId': token.userId
});
const user = await User.findById(token.userId);
if (!user) {
return res.status(401).json({ error: 'user_not_found' });
}
// 根据scope返回用户信息
const userInfo: any = {};
if (token.scope.includes('openid')) {
userInfo.sub = user._id;
}
if (token.scope.includes('profile')) {
userInfo.name = user.username;
userInfo.fullname = user.fullname;
}
if (token.scope.includes('email')) {
userInfo.email = user.email;
}
if (token.scope.includes('firstname')) {
userInfo.firstname = user.username;
}
if (token.scope.includes('lastname')) {
userInfo.lastname = user.fullname;
}
if (token.scope.includes('username')) {
userInfo.username = user.username;
}
console.log('user', user);
console.log('userInfo', userInfo);
console.log('Token scopes:', token.scope);
console.log('最终返回的 userInfo:', userInfo);
res.json(userInfo);
} catch (error) {
res.status(500).json({ error: 'server_error6' });
}
}
async getMoodleSesskey(req: Request, res: Response) {
console.log('Received request for moodle-sesskey'); // 请求开始日志
try {
console.log('Fetching Moodle login page...');
const response = await fetch('https://m.d1kt.cn/login/index.php');
const html = await response.text();
console.log('Moodle response received, length:', html.length); // 检查是否获取到响应
const sesskeyMatch = html.match(/sesskey":"([^"]+)/);
console.log('Sesskey match result:', sesskeyMatch); // 检查正则匹配结果
const sesskey = sesskeyMatch ? sesskeyMatch[1] : '';
if (!sesskey) {
console.log('No sesskey found in response'); // 未找到 sesskey
return res.status(500).json({ error: 'Failed to get sesskey' });
}
console.log('Successfully got sesskey:', sesskey); // 成功获取 sesskey
res.json({ sesskey });
} catch (error) {
console.error('Get sesskey error:', error); // 详细的错误信息
res.status(500).json({ error: 'Failed to get sesskey' });
}
}
}