chore: oauth-second

This commit is contained in:
xiaojunnuo
2025-11-26 23:25:51 +08:00
parent 5a148aa3b9
commit e9427b4694
14 changed files with 306 additions and 47 deletions

View File

@@ -8,5 +8,6 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
},
"editor.tabSize": 2
}

View File

@@ -229,3 +229,14 @@ export class SysSafeSetting extends BaseSettings {
}
export class SysOauthSetting extends BaseSettings {
static __title__ = 'OAuth设置';
static __key__ = 'sys.oauth';
static __access__ = 'private';
oauths: Record<string, {
type: string;
title: string;
addonId: number;
}> = {};
}

View File

@@ -187,4 +187,14 @@ export class AddonService extends BaseService<AddonEntity> {
});
return this.buildAddonInstanceConfig(res);
}
async getOneByType(req:{addonType:string,type:string,userId:number}) {
return await this.repository.findOne({
where: {
addonType: req.addonType,
type: req.type,
userId: req.userId
}
});
}
}

View File

@@ -17,6 +17,7 @@
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
"@typescript-eslint/no-unused-vars": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}
}

View File

@@ -7,4 +7,5 @@ export * from "./qiniu/index.js";
export * from "./ctyun/index.js";
export * from "./oss/index.js";
export * from "./s3/index.js";
export * from "./lib/index.js";
export * from "./lib/index.js";
export * from "./service/index.js";

View File

@@ -0,0 +1 @@
export * from "./site-info.js";

View File

@@ -0,0 +1,7 @@
export type SiteInfo = {
siteUrl: string;
};
export interface ISiteInfoGetter {
getSiteInfo(): Promise<SiteInfo>;
}

View File

@@ -0,0 +1,68 @@
import { AddonService, BaseController, Constants, newAddon, SysInstallInfo, SysOauthSetting, SysSettingsService } from "@certd/lib-server";
import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core";
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
import { AddonGetterService } from "../../../modules/pipeline/service/addon-getter-service.js";
/**
*/
@Provide()
@Controller('/api/connect')
export class ConnectController extends BaseController {
@Inject()
addonGetterService: AddonGetterService;
@Inject()
sysSettingsService: SysSettingsService;
private async getOauthProvider(type:string){
const oauthSetting = await this.sysSettingsService.getSetting<SysOauthSetting>(SysOauthSetting);
const setting = oauthSetting?.oauths?.[type||""]
if (!setting) {
throw new Error(`未配置该OAuth类型:${type}`);
}
const addon = await this.addonGetterService.getAddonById(setting.addonId, true, 0);
if(!addon) {
throw new Error("初始化OAuth插件失败");
}
return addon as IOauthProvider;
}
@Post('/login', { summary: Constants.per.guest })
public async login(@Query(ALL) body: {type:string}) {
const addon = await this.getOauthProvider(body.type);
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo?.bindUrl || "";
//构造登录url
const redirectUrl = `${bindUrl}#/auth/callback/${body.type}`;
const loginUrl = await addon.buildLoginUrl({ redirectUri: redirectUrl});
return this.ok(loginUrl);
}
@Post('/callback', { summary: Constants.per.guest })
public async callback(@Query(ALL) body: any) {
//处理登录回调
const addon = await this.getOauthProvider(body.type);
const tokenRes = await addon.onCallback({
code: body.code,
redirectUri: body.redirectUri,
state: body.state,
});
const userInfo = tokenRes.userInfo;
return this.ok(tokenRes);
}
@Post('/bind', { summary: Constants.per.guest })
public async bind(@Body(ALL) body: any) {
const autoRegister = body.autoRegister || false;
const bindInfo = body.bind || {};
//处理登录回调
return this.ok(1);
}
}

View File

@@ -1,30 +0,0 @@
import { BaseController, Constants } from "@certd/lib-server";
import { ALL, Body, Controller, Get, Post, Provide, Query } from "@midwayjs/core";
/**
*/
@Provide()
@Controller('/api/connect')
export class LoginController extends BaseController {
@Get('/login', { summary: Constants.per.guest })
public async login(@Query(ALL) body: any) {
//构造登录url
return this.ok(1);
}
@Get('/callback', { summary: Constants.per.guest })
public async callback(@Query(ALL) body: any) {
//处理登录回调
return this.ok(1);
}
@Post('/bind', { summary: Constants.per.guest })
public async bind(@Body(ALL) body: any) {
const autoRegister = body.autoRegister || false;
const bindInfo = body.bind || {};
//处理登录回调
return this.ok(1);
}
}

View File

@@ -0,0 +1,22 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('cd_oauth_bind')
export class OauthBindEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ name: 'type', comment: '第三方类型' })
type: string; // oidc, wechat, github, gitee , qq , alipay
@Column({ name: 'open_id', comment: '第三方openid' })
openId: string;
@Column({ name: 'create_time',comment: '创建时间', default: () => 'CURRENT_TIMESTAMP',})
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间',default: () => 'CURRENT_TIMESTAMP',})
updateTime: Date;
}

View File

@@ -0,0 +1,20 @@
import { SysSettingsService, SysInstallInfo } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { SiteInfo ,ISiteInfoGetter} from "@certd/plugin-lib";
@Provide("siteInfoGetter")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class SiteInfoGetter implements ISiteInfoGetter{
@Inject()
sysSettingsService: SysSettingsService;
async getSiteInfo(): Promise<SiteInfo> {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
return {
siteUrl: installInfo?.bindUrl || "",
}
}
}

View File

@@ -1,14 +1,23 @@
export interface OauthProvider {
buildLoginUrl: (params: { redirectUri: string }) => string;
handleCallback: (params: { code: string; redirectUri: string }) => Promise<{
accessToken: string;
refreshToken: string;
expiresIn: number;
idToken: string;
scope: string;
tokenType: string;
}>;
bind: (params: {
export type OnCallbackReq = {
code: string;
redirectUri: string;
state: string;
}
export type OauthToken = {
userInfo: {
openId: string;
nickName: string;
avatar: string;
},
token: {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
}
export type OnBindReq = {
accessToken: string;
refreshToken: string;
expiresIn: number;
@@ -16,9 +25,14 @@ export interface OauthProvider {
scope: string;
tokenType: string;
bindInfo: any;
}) => Promise<{
}
export type OnBindReply = {
success: boolean;
message: string;
}>;
}
export interface IOauthProvider {
buildLoginUrl: (params: { redirectUri: string }) => Promise<string>;
onCallback: (params: OnCallbackReq) => Promise<OauthToken>;
onBind: (params: OnBindReq) => Promise<OnBindReply>;
}

View File

@@ -0,0 +1,2 @@
export * from './api.js'
export * from './oidc/plugin-oidc.js'

View File

@@ -0,0 +1,131 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { IOauthProvider, OnBindReq, OnCallbackReq } from "../api.js";
@IsAddon({
addonType: "oauth",
name: 'oidc',
title: 'OpenId connect 认证',
desc: '',
showTest: false,
})
export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
@AddonInput({
title: "ClientId",
helper: "ClientId / appId",
required: true,
})
clientId = "";
@AddonInput({
title: "ClientSecretKey",
component: {
placeholder: "ClientSecretKey / appSecretKey",
},
required: true,
})
clientSecretKey = "";
@AddonInput({
title: "服务地址",
helper: "Issuer地址",
component: {
placeholder: "https://oidc.example.com/oidc",
},
required: true,
})
issuerUrl = "";
async getClient() {
const client = await import('openid-client')
let server = new URL(this.issuerUrl)// Authorization Server's Issuer Identifier
let config = await client.discovery(
server,
this.clientId,
this.clientSecretKey,
)
// console.log(config.serverMetadata())
return {
config,
client
}
}
async onCallback(req: OnCallbackReq) {
const { config, client } = await this.getClient()
const currentUrl = new URL(req.redirectUri)
let tokens: any = await client.authorizationCodeGrant(
config,
currentUrl,
{
pkceCodeVerifier: req.code,
expectedState: req.state,
},
)
console.log('Token Endpoint Response', tokens)
const claims = tokens.claims()
return {
token:{
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
},
userInfo: {
openId: claims.sub,
nickName: claims.nickname,
avatar: claims.picture,
},
}
};
async onBind(req: OnBindReq) {
return {
success: false,
message: '绑定失败',
}
}
async buildLoginUrl(params: { redirectUri: string }) {
const { config, client } = await this.getClient()
let redirect_uri = new URL(params.redirectUri)
let scope = 'openid profile' // Scope of the access request
/**
* PKCE: The following MUST be generated for every redirect to the
* authorization_endpoint. You must store the code_verifier and state in the
* end-user session such that it can be recovered as the user gets redirected
* from the authorization server back to your application.
*/
let code_verifier = client.randomPKCECodeVerifier()
let code_challenge = await client.calculatePKCECodeChallenge(code_verifier)
let state = client.randomState()
let parameters: any = {
redirect_uri,
scope,
code_challenge,
code_challenge_method: 'S256',
state,
}
// if (!config.serverMetadata().supportsPKCE()) {
// /**
// * We cannot be sure the server supports PKCE so we're going to use state too.
// * Use of PKCE is backwards compatible even if the AS doesn't support it which
// * is why we're using it regardless. Like PKCE, random state must be generated
// * for every redirect to the authorization_endpoint.
// */
// parameters.state = client.randomState()
// }
let redirectTo = client.buildAuthorizationUrl(config, parameters)
// now redirect the user to redirectTo.href
console.log('redirecting to', redirectTo.href)
return redirectTo.href;
}
}