feat: 新增oauth相关的实体定义及类型定义

This commit is contained in:
Pan Qiancheng 2025-10-22 09:13:14 +08:00 committed by qcqcqc
parent b1c33ebd9c
commit 9299a2ba66
9 changed files with 503 additions and 0 deletions

View File

@ -0,0 +1,71 @@
import { String, Int, Text, Image } from 'oak-domain/lib/types/DataType';
import { Schema as User } from './User';
import { Schema as Token } from './Token';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { ActionDef, Index } from 'oak-domain/lib/types';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
import { Schema as System } from './System';
import { StringListJson } from '../types/datatype';
// oauth 应用表
// RFC 6749 Section 2 (Client Registration) OAuth 客户端注册表
export interface Schema extends EntityShape {
// clientId直接复用entity的id属性
clientSecret: String<512>;
system: System;
name: String<64>;
description?: Text;
redirectUris?: StringListJson;
logo?: String<512>;
isConfidential: Boolean;
scopes?: StringListJson;
};
export type Action = AbleAction;
export const AbleActionDef: ActionDef<AbleAction, AbleState> = makeAbleActionDef('enabled');
export const entityDesc: EntityDesc<Schema, Action, '', {
ableState: AbleState;
}> = {
locales: {
zh_CN: {
name: 'Oauth应用',
attr: {
ableState: '是否可用',
// clientId直接复用entity的id属性
clientSecret: '客户端密钥',
system: '所属系统',
name: '应用名称',
description: '应用描述',
redirectUris: '重定向 URI',
logo: '应用 Logo',
isConfidential: '是否保密',
scopes: '应用权限范围',
},
action: {
enable: '启用',
disable: '禁用',
},
v: {
ableState: {
enabled: '可用的',
disabled: '禁用的',
}
}
},
},
style: {
icon: {
enable: '',
disable: '',
},
color: {
ableState: {
enabled: '#008000',
disabled: '#A9A9A9'
}
}
}
};

View File

@ -0,0 +1,56 @@
import { String, Int, Text, Image, Datetime } from 'oak-domain/lib/types/DataType';
import { Schema as User } from './User';
import { Schema as Token } from './Token';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { ActionDef, Index } from 'oak-domain/lib/types';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
import { Schema as System } from './System';
import { Schema as OauthApplication } from './OauthApplication';
import { StringListJson } from '../types/datatype';
// oauth 提供方生成的授权码记录
// RFC 6749 Section 1.3.1 (Authorization Code) 授权码表
export interface Schema extends EntityShape {
code: String<128>;
application: OauthApplication;
user: User;
redirectUri: String<512>;
scope: StringListJson;
// PKCE 扩展 (RFC 7636) 暂时用不到
codeChallenge?: String<128>;
codeChallengeMethod?: String<10>; // "plain" or "S256"
// 生命周期管理
expiresAt: Datetime; // 推荐值10分钟以内最长不超过1小时
// 使用时间
usedAt?: Datetime; // 如果被使用过,记录使用时间,否则为空
};
// PKCE 扩展 (RFC 7636)
// 1. 客户端生成 code_verifier (随机字符串)
// 2. 计算 code_challenge = BASE64URL(SHA256(code_verifier))
// 3. 授权请求携带 code_challenge
// 4. 存储到数据库
// 5. 令牌交换时验证 code_verifier
export const entityDesc: EntityDesc<Schema, '', '', {
}> = {
locales: {
zh_CN: {
name: '授权码',
attr: {
code: '授权码',
application: 'Oauth应用',
user: '用户',
redirectUri: '重定向 URI',
scope: '权限范围',
codeChallenge: '代码挑战',
codeChallengeMethod: '代码挑战方法',
expiresAt: '过期时间',
usedAt: '使用时间',
},
},
},
};

View File

@ -0,0 +1,86 @@
import { String, Int, Text, Image, Datetime } from 'oak-domain/lib/types/DataType';
import { Schema as User } from './User';
import { Schema as Token } from './Token';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { ActionDef, Index } from 'oak-domain/lib/types';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
import { Schema as System } from './System';
import { Schema as OauthApplication } from './OauthApplication';
import { StringListJson } from '../types/datatype';
// oauth 提供方表
// RFC 6749 Section 1.4 (Access Token) && RFC 6749 Section 1.5 (Refresh Token) 令牌表
export interface Schema extends EntityShape {
system: System;
name: String<64>;
type: "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk";
logo?: String<512>;
// OAuth 端点 (RFC 6749 Section 3)
authorizationEndpoint: String<512>; // RFC 6749 Section 3.1
tokenEndpoint: String<512>; // RFC 6749 Section 3.2
userInfoEndpoint?: String<512>; // OpenID Connect
revokeEndpoint?: String<512>; // RFC 7009
// 客户端凭证 (RFC 6749 Section 2.3)
clientId: String<512>;
clientSecret: String<512>; // 授权服务器注册的 client_secret 目前没有公共场景使用 PKCE所以必填
redirectUri: String<512>; // 授权后时指定的重定向 URI
scopes?: StringListJson; // 请求的权限范围
// 配置选项
autoRegister: Boolean; // 是否自动注册用户
};
export type Action = AbleAction;
export const AbleActionDef: ActionDef<AbleAction, AbleState> = makeAbleActionDef('enabled');
export const entityDesc: EntityDesc<Schema, Action, '', {
ableState: AbleState;
}> = {
locales: {
zh_CN: {
name: 'Oauth提供者配置',
attr: {
system: '所属系统',
ableState: '是否可用',
name: '名称',
type: '类型',
logo: 'Logo',
authorizationEndpoint: '授权端点',
tokenEndpoint: '令牌端点',
userInfoEndpoint: '用户信息端点',
revokeEndpoint: '吊销端点',
clientId: '客户端 ID',
clientSecret: '客户端密钥',
redirectUri: '重定向 URI',
scopes: '权限范围',
autoRegister: '自动注册用户',
},
action: {
enable: '启用',
disable: '禁用',
},
v: {
ableState: {
enabled: '可用的',
disabled: '禁用的',
}
}
},
},
style: {
icon: {
enable: '',
disable: '',
},
color: {
ableState: {
enabled: '#008000',
disabled: '#A9A9A9'
}
}
}
};

View File

@ -0,0 +1,44 @@
import { String, Int, Text, Image, Datetime } from 'oak-domain/lib/types/DataType';
import { Schema as User } from './User';
import { Schema as Token } from './Token';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { ActionDef, Index } from 'oak-domain/lib/types';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
import { Schema as System } from './System';
import { Schema as OauthProvider } from './OauthProvider';
import { Schema as Application } from './Application';
// oauth 应用方预存储state做校验
// RFC 6749 Section 1.3.1 (Authorization Code) 第三方授权码表
export interface Schema extends EntityShape {
state: String<32>; // 防止CSRF攻击的状态参数
type: "login" | "bind" // 操作类型,登录或绑定
provider: OauthProvider;
user?: User; // 如果是已经登录的用户进行的操作,则会记录
usedAt?: Datetime
};
export const entityDesc: EntityDesc<Schema, '', '', {
}> = {
locales: {
zh_CN: {
name: '授权码',
attr: {
state: '状态',
provider: 'Oauth提供者配置',
type: '操作类型',
user: '操作者',
usedAt: '被使用日期'
},
v: {
type: {
login: '登录',
bind: '绑定',
}
}
},
},
};

View File

@ -0,0 +1,44 @@
import { String, Int, Text, Image, Datetime } from 'oak-domain/lib/types/DataType';
import { Schema as User } from './User';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { ActionDef, Index } from 'oak-domain/lib/types';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
import { Schema as System } from './System';
import { Schema as OauthApplication } from './OauthApplication';
import { Schema as OauthAuthorizationCode } from './OauthAuthorizationCode';
// 由 oauth 提供方下发accessToken记录
// RFC 6749 Section 1.4 (Access Token) && RFC 6749 Section 1.5 (Refresh Token) 令牌表
export interface Schema extends EntityShape {
accessToken: String<1024>;
refreshToken: String<1024>;
user: User;
code: OauthAuthorizationCode; // 是用的什么授权码换取的令牌
accessExpiresAt: Datetime; // 推荐值不超过1小时
refreshExpiresAt: Datetime; // 推荐值不超过2个月
revokedAt?: Datetime; // 如果被吊销,记录吊销时间,否则为空
// 审计相关,需要手动更新
lastUsedAt?: Datetime;
};
export const entityDesc: EntityDesc<Schema, '', '', {
}> = {
locales: {
zh_CN: {
name: 'Oauth令牌',
attr: {
accessToken: '访问令牌',
refreshToken: '刷新令牌',
user: '用户',
code: '授权码',
accessExpiresAt: '访问令牌过期时间',
refreshExpiresAt: '刷新令牌过期时间',
revokedAt: '吊销时间',
lastUsedAt: '最后使用时间',
},
},
},
};

55
src/entities/OauthUser.ts Normal file
View File

@ -0,0 +1,55 @@
import { String, Int, Text, Image, Datetime } from 'oak-domain/lib/types/DataType';
import { Schema as User } from './User';
import { Schema as Token } from './Token';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { ActionDef, Index } from 'oak-domain/lib/types';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
import { Schema as System } from './System';
import { Schema as OauthApplication } from './OauthApplication';
import { Schema as OauthProvider } from './OauthProvider';
import { Schema as Application } from './Application';
import { Schema as OauthState } from './OauthState'
// oauth 应用方存储用户认证后的基本信息
// 用户连接表 OAuth User Connections
export interface Schema extends EntityShape {
user?: User;
application: Application;
providerConfig: OauthProvider;
providerUserId: String<256>; // 第三方提供者的用户ID
rawUserInfo: Object; // 存储从第三方获取的原始用户信息
// 令牌 10-21: 不知道为什么老超出,设置大一点
accessToken: String<1024>;
refreshToken?: String<1024>;
accessExpiresAt: Datetime;
refreshExpiresAt?: Datetime;
tokens: Token[]; // 该授权码兑换的令牌列表
state: OauthState
};
export const entityDesc: EntityDesc<Schema, '', '', {
}> = {
locales: {
zh_CN: {
name: '用户登录连接',
attr: {
user: '用户',
providerConfig: 'Oauth提供者配置',
providerUserId: '提供者用户ID',
rawUserInfo: '原始用户信息',
accessToken: '访问令牌',
refreshToken: '刷新令牌',
accessExpiresAt: '访问令牌过期时间',
refreshExpiresAt: '刷新令牌过期时间',
application: '应用',
tokens: '令牌列表',
state: '认证时使用的state'
},
},
},
};

View File

@ -0,0 +1,36 @@
import { String, Int, Text, Image, Datetime } from 'oak-domain/lib/types/DataType';
import { Schema as User } from './User';
import { Schema as Token } from './Token';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { ActionDef, Index } from 'oak-domain/lib/types';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
import { Schema as System } from './System';
import { Schema as OauthApplication } from './OauthApplication';
import { Schema as OauthToken } from './OauthToken';
// oauth 提供方用户授权记录
// 用户授权记录表 OAuth Authorization Records
export interface Schema extends EntityShape {
user: User;
application: OauthApplication;
authorizedAt: Datetime; // 用户首次授权时间
token: OauthToken; // 关联的令牌
};
export const entityDesc: EntityDesc<Schema, '', '', {
}> = {
locales: {
zh_CN: {
// 用户可以查看和管理已授权的应用
name: '用户授权记录',
attr: {
user: '用户',
application: 'Oauth应用',
authorizedAt: '首次授权时间',
token: '关联的令牌',
},
},
},
};

View File

@ -254,6 +254,19 @@ export class Token<ED extends EntityDict> extends Feature {
this.checkNeedSetPassword();
}
async loginByOAuth(code: string, state: string) {
const env = await this.environment.getEnv();
const { result } = await this.cache.exec('loginByOauth', {
env: env as WebEnv,
code,
state,
});
this.tokenValue = result;
await this.storage.save(LOCAL_STORAGE_KEYS.token, result);
this.publish();
this.checkNeedSetPassword();
}
async loginWechat(code: string, params?: { wechatLoginId?: string }) {
const env = await this.environment.getEnv();
const { result } = await this.cache.exec('loginWechat', {

98
src/types/OpenID.ts Normal file
View File

@ -0,0 +1,98 @@
/**
* OpenID Connect UserInfo
* OpenID Connect Core 1.0
*/
interface OpenIDUserInfo {
// 必需字段
/** Subject - 用户的唯一标识符 */
sub: string;
// 基本个人信息 (profile scope)
/** 全名 */
name?: string;
/** 名 */
given_name?: string;
/** 姓 */
family_name?: string;
/** 中间名 */
middle_name?: string;
/** 昵称 */
nickname?: string;
/** 首选用户名 */
preferred_username?: string;
/** 个人资料页面 URL */
profile?: string;
/** 头像图片 URL */
picture?: string;
/** 个人网站 URL */
website?: string;
/** 性别 */
gender?: string;
/** 出生日期 (YYYY-MM-DD) */
birthdate?: string;
/** 时区信息 (例如: "America/Los_Angeles") */
zoneinfo?: string;
/** 语言区域设置 (例如: "en-US") */
locale?: string;
/** 信息最后更新时间 (Unix 时间戳,秒) */
updated_at?: number;
// 电子邮件 (email scope)
/** 电子邮件地址 */
email?: string;
/** 电子邮件是否已验证 */
email_verified?: boolean;
// 电话号码 (phone scope)
/** 电话号码 (E.164 格式) */
phone_number?: string;
/** 电话号码是否已验证 */
phone_number_verified?: boolean;
// 地址 (address scope)
/** 邮寄地址 */
address?: OpenIDAddress;
// 允许其他自定义声明
[key: string]: any;
}
/**
* OpenID Connect
*/
interface OpenIDAddress {
/** 完整格式化的邮寄地址,可能包含换行符 */
formatted?: string;
/** 街道地址,可能包含门牌号和街道名称 */
street_address?: string;
/** 城市或地区 */
locality?: string;
/** 州、省、县或地区 */
region?: string;
/** 邮政编码 */
postal_code?: string;
/** 国家名称 */
country?: string;
}
/**
* UserInfo
*/
interface UserInfoRequest {
/** Access Token (通常在 Authorization header 中) */
access_token: string;
}
/**
* UserInfo
*/
interface UserInfoError {
/** 错误代码 */
error: 'invalid_token' | 'insufficient_scope' | 'invalid_request';
/** 错误描述 */
error_description?: string;
/** 错误相关的 URI */
error_uri?: string;
}
export { OpenIDUserInfo, OpenIDAddress, UserInfoRequest, UserInfoError };