feat: 新增oauth相关的实体定义及类型定义
This commit is contained in:
parent
b1c33ebd9c
commit
9299a2ba66
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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: '使用时间',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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: '绑定',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -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: '最后使用时间',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -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'
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -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: '关联的令牌',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
Loading…
Reference in New Issue