casualroom/apps/fenghuo/web/lib/auth/oidc-client.ts

222 lines
6.4 KiB
TypeScript
Executable File
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 { Prisma } from "@fenghuo/db";
// apps/web/lib/oidc-client.ts
interface OIDCTokenResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token?: string;
id_token?: string;
scope: string;
}
interface OIDCUserInfo {
sub: string;
username: string;
email: string;
name: string;
department?: string;
roles?: string[];
[key: string]: any;
}
interface OIDCError {
error: string;
error_description?: string;
}
export class OIDCClient {
private baseUrl: string;
private clientId: string;
private clientSecret?: string;
constructor() {
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; // 这里配置oidc服务器的地址
this.clientId = process.env.NEXT_PUBLIC_OIDC_CLIENT_ID || 'web-app'; // 这里配置对应的id
this.clientSecret = process.env.NEXT_PUBLIC_OIDC_CLIENT_SECRET;
}
/**
* 使用用户名密码登录获取访问令牌
*/
async loginWithPassword(username: string, password: string): Promise<OIDCTokenResponse> {
const response = await fetch(`${this.baseUrl}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'password',
username,
password,
client_id: this.clientId,
scope: 'openid profile email',
}),
});
if (!response.ok) {
const error: OIDCError = await response.json();
throw new Error(error.error_description || error.error || '登录失败');
}
return response.json();
}
/**
* 使用刷新令牌获取新的访问令牌
*/
async refreshToken(refreshToken: string): Promise<OIDCTokenResponse> {
const response = await fetch(`${this.baseUrl}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': this.clientSecret ?
`Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}` : '',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientId,
}),
});
if (!response.ok) {
const error: OIDCError = await response.json();
throw new Error(error.error_description || error.error || '刷新令牌失败');
}
return response.json();
}
/**
* 获取用户信息
*/
async getUserInfo(accessToken: string): Promise<OIDCUserInfo> {
const response = await fetch(`${this.baseUrl}/auth/userinfo`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
const error: OIDCError = await response.json();
throw new Error(error.error_description || error.error || '获取用户信息失败');
}
return response.json();
}
/**
* 注销登录
*/
async logout(accessToken?: string): Promise<void> {
if (accessToken) {
await fetch(`${this.baseUrl}/auth/revoke`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${accessToken}`,
},
body: new URLSearchParams({
token: accessToken,
token_type_hint: 'access_token',
}),
});
}
}
/**
* 注册新用户
*/
async register(userData: Prisma.UserCreateArgs): Promise<{ success: boolean; message: string }> {
const response = await fetch(`${this.baseUrl}/trpc/user.create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
json: userData,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || '注册失败');
}
const result = await response.json();
return {
success: true,
message: '注册成功',
};
}
/**
* 构建授权 URL用于跳转到登录页面
*/
buildAuthorizationUrl(options: {
redirectUri: string;
state?: string;
scope?: string;
nonce?: string;
}): string {
const { redirectUri, state = this.generateState(), scope = 'openid profile email', nonce = this.generateNonce() } = options;
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: redirectUri,
scope,
state,
nonce,
});
return `${this.baseUrl}/auth/auth?${params.toString()}`;
}
/**
* 处理授权回调(交换授权码获取令牌)
*/
async handleAuthorizationCallback(
authorizationCode: string,
redirectUri: string,
state?: string
): Promise<OIDCTokenResponse> {
const response = await fetch(`${this.baseUrl}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: redirectUri,
client_id: this.clientId,
...(this.clientSecret && { client_secret: this.clientSecret }),
}),
});
if (!response.ok) {
const error: OIDCError = await response.json();
throw new Error(error.error_description || error.error || '授权码交换失败');
}
return response.json();
}
/**
* 生成随机状态值防CSRF
*/
private generateState(): string {
return btoa(Math.random().toString(36).substr(2, 9));
}
/**
* 生成随机 nonce 值
*/
private generateNonce(): string {
return btoa(Math.random().toString(36).substr(2, 9));
}
}
export const oidcClient = new OIDCClient();