mirror of
https://github.com/certd/certd.git
synced 2026-04-15 05:00:52 +08:00
chore: oauth-second
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -8,5 +8,6 @@
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
},
|
||||
"editor.tabSize": 2
|
||||
}
|
||||
@@ -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;
|
||||
}> = {};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
1
packages/plugins/plugin-lib/src/service/index.ts
Normal file
1
packages/plugins/plugin-lib/src/service/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./site-info.js";
|
||||
7
packages/plugins/plugin-lib/src/service/site-info.ts
Normal file
7
packages/plugins/plugin-lib/src/service/site-info.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type SiteInfo = {
|
||||
siteUrl: string;
|
||||
};
|
||||
|
||||
export interface ISiteInfoGetter {
|
||||
getSiteInfo(): Promise<SiteInfo>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 || "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './api.js'
|
||||
export * from './oidc/plugin-oidc.js'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user