111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
|
|
import { z } from 'zod';
|
|||
|
|
export const codeChallengeMethods = z.enum(['plain', 'S256']);
|
|||
|
|
|
|||
|
|
// 授权请求Schema
|
|||
|
|
export const authorizationRequestSchema = z.object({
|
|||
|
|
response_type: z.string().min(1, '响应类型不能为空'),
|
|||
|
|
client_id: z.string().min(1, '客户端ID不能为空'),
|
|||
|
|
redirect_uri: z.string().url('重定向URI必须是有效的URL'),
|
|||
|
|
scope: z.string().min(1, '作用域不能为空'),
|
|||
|
|
state: z.string().optional(),
|
|||
|
|
nonce: z.string().optional(),
|
|||
|
|
code_challenge: z.string().optional(),
|
|||
|
|
code_challenge_method: codeChallengeMethods.optional(),
|
|||
|
|
prompt: z.string().optional(),
|
|||
|
|
max_age: z.number().int().positive().optional(),
|
|||
|
|
id_token_hint: z.string().optional(),
|
|||
|
|
login_hint: z.string().optional(),
|
|||
|
|
acr_values: z.string().optional(),
|
|||
|
|
}).strict();
|
|||
|
|
|
|||
|
|
// 令牌请求Schema
|
|||
|
|
export const tokenRequestSchema = z.object({
|
|||
|
|
grant_type: z.string().min(1, '授权类型不能为空'),
|
|||
|
|
code: z.string().optional(),
|
|||
|
|
redirect_uri: z.string().url('重定向URI必须是有效的URL').optional().or(z.literal('')),
|
|||
|
|
client_id: z.string().min(1, '客户端ID不能为空'),
|
|||
|
|
client_secret: z.string().optional(),
|
|||
|
|
refresh_token: z.string().optional(),
|
|||
|
|
scope: z.string().optional(),
|
|||
|
|
code_verifier: z.string().optional(),
|
|||
|
|
}).strict();
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 令牌撤销请求Schema
|
|||
|
|
export const revokeTokenRequestSchema = z.object({
|
|||
|
|
token: z.string().min(1, '令牌不能为空'),
|
|||
|
|
token_type_hint: z.enum(['access_token', 'refresh_token']).optional(),
|
|||
|
|
client_id: z.string().optional(),
|
|||
|
|
}).strict();
|
|||
|
|
|
|||
|
|
// 令牌内省请求Schema
|
|||
|
|
export const introspectTokenRequestSchema = z.object({
|
|||
|
|
token: z.string().min(1, '令牌不能为空'),
|
|||
|
|
token_type_hint: z.enum(['access_token', 'refresh_token']).optional(),
|
|||
|
|
client_id: z.string().optional(),
|
|||
|
|
}).strict();
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 查询参数解析Schema(用于解析URL参数)
|
|||
|
|
export const authorizationQuerySchema = z.record(z.string(), z.union([z.string(), z.array(z.string())])).transform((data) => {
|
|||
|
|
// 将数组参数转换为单个字符串(取第一个值)
|
|||
|
|
const normalized: Record<string, string> = {};
|
|||
|
|
for (const [key, value] of Object.entries(data)) {
|
|||
|
|
if (Array.isArray(value)) {
|
|||
|
|
// 处理数组,取第一个值,如果为空则设为空字符串
|
|||
|
|
normalized[key] = value[0] || '';
|
|||
|
|
} else {
|
|||
|
|
// 处理字符串值
|
|||
|
|
normalized[key] = value || '';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return normalized;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Form data解析Schema
|
|||
|
|
export const tokenFormDataSchema = z.instanceof(FormData).transform((formData) => {
|
|||
|
|
const result: Record<string, string> = {};
|
|||
|
|
for (const [key, value] of formData.entries()) {
|
|||
|
|
// 只处理字符串值,忽略File类型
|
|||
|
|
if (typeof value === 'string') {
|
|||
|
|
result[key] = value;
|
|||
|
|
} else if (value instanceof File) {
|
|||
|
|
// 如果是文件,将文件名作为值(通常不应该在token请求中出现)
|
|||
|
|
result[key] = value.name || '';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// HTTP Authorization header Schema
|
|||
|
|
export const bearerTokenSchema = z.string().regex(/^Bearer\s+(.+)$/, '无效的Bearer令牌格式').transform((auth) => {
|
|||
|
|
return auth.replace(/^Bearer\s+/, '');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const basicAuthSchema = z.string().regex(/^Basic\s+(.+)$/, '无效的Basic认证格式').transform((auth) => {
|
|||
|
|
try {
|
|||
|
|
const base64Part = auth.replace(/^Basic\s+/, '');
|
|||
|
|
if (!base64Part) {
|
|||
|
|
throw new Error('Basic认证缺少凭证部分');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const decoded = atob(base64Part);
|
|||
|
|
const colonIndex = decoded.indexOf(':');
|
|||
|
|
|
|||
|
|
if (colonIndex === -1) {
|
|||
|
|
// 如果没有冒号,整个字符串作为用户名,密码为空
|
|||
|
|
return { username: decoded, password: '' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const username = decoded.substring(0, colonIndex);
|
|||
|
|
const password = decoded.substring(colonIndex + 1);
|
|||
|
|
return { username, password };
|
|||
|
|
} catch (error) {
|
|||
|
|
throw new Error(`无效的Basic认证编码: ${error instanceof Error ? error.message : '未知错误'}`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|