chore: 优化oidc登录

This commit is contained in:
xiaojunnuo
2025-11-28 01:42:42 +08:00
parent 2fabee647a
commit 21585ca565
17 changed files with 264 additions and 130 deletions
@@ -1,5 +1,5 @@
import { PermissionException, ValidateException } from './exception/index.js'; import { PermissionException, ValidateException } from './exception/index.js';
import { In, Repository, SelectQueryBuilder } from 'typeorm'; import { FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm';
import { Inject } from '@midwayjs/core'; import { Inject } from '@midwayjs/core';
import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js'; import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
@@ -238,4 +238,8 @@ export abstract class BaseService<T> {
await this.delete(ids); await this.delete(ids);
} }
async findOne(options: FindOneOptions<T>) {
return await this.getRepository().findOne(options);
}
} }
@@ -31,6 +31,7 @@ export type AddonDefine = Registrable & {
[key: string]: AddonInputDefine; [key: string]: AddonInputDefine;
}; };
showTest?: boolean; showTest?: boolean;
icon?: string;
}; };
export type AddonInstanceConfig = { export type AddonInstanceConfig = {
@@ -1,8 +1,8 @@
<template> <template>
<div id="userLayout" :class="['user-layout-wrapper']"> <div id="userLayout" :class="['user-layout-wrapper']">
<div class="login-container flex-center"> <div class="login-container flex justify-start">
<div class="user-layout-content flex-center flex-col"> <div class="user-layout-content flex-col justify-start">
<div class="top flex flex-col items-center justify-center"> <div class="top flex flex-col items-center justify-start">
<div class="header flex flex-row items-center"> <div class="header flex flex-row items-center">
<img :src="siteInfo.loginLogo" class="logo" alt="logo" /> <img :src="siteInfo.loginLogo" class="logo" alt="logo" />
<span class="title"></span> <span class="title"></span>
@@ -10,8 +10,9 @@
<div class="desc">{{ siteInfo.slogan }}</div> <div class="desc">{{ siteInfo.slogan }}</div>
</div> </div>
<router-view /> <div class="flex-1 flex flex-col justify-start items-center">
<router-view />
</div>
<div class="footer"> <div class="footer">
<div class="copyright"> <div class="copyright">
<span v-if="!settingStore.isComm"> <span v-if="!settingStore.isComm">
@@ -57,6 +57,7 @@ export default {
passwordPlaceholder: "Please enter your password", passwordPlaceholder: "Please enter your password",
mobilePlaceholder: "Please enter your mobile number", mobilePlaceholder: "Please enter your mobile number",
loginButton: "Log In", loginButton: "Log In",
bindButton: "Bind Account",
forgotPassword: "Forgot password?", forgotPassword: "Forgot password?",
forgotAdminPassword: "Forgot admin password?", forgotAdminPassword: "Forgot admin password?",
registerLink: "Register", registerLink: "Register",
@@ -760,6 +760,13 @@ export default {
fixedCertExpireDays: "Fixed Cert Expire Days", fixedCertExpireDays: "Fixed Cert Expire Days",
fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display", fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display",
fixedCertExpireDaysRecommend: "Recommend 90", fixedCertExpireDaysRecommend: "Recommend 90",
enableOauth: "Enable OAuth2 Login",
oauthEnabledHelper: "Whether to enable OAuth2 login",
oauthProviders: "OAuth2 Login Providers",
oauthType: "OAuth2 Login Type",
oauthConfig: "OAuth2 Login Config",
oauthProviderSelectorPlaceholder: "Please select OAuth2 login provider",
}, },
}, },
modal: { modal: {
@@ -57,6 +57,7 @@ export default {
passwordPlaceholder: "请输入密码", passwordPlaceholder: "请输入密码",
mobilePlaceholder: "请输入手机号", mobilePlaceholder: "请输入手机号",
loginButton: "登录", loginButton: "登录",
bindButton: "绑定账号",
forgotPassword: "忘记密码?", forgotPassword: "忘记密码?",
forgotAdminPassword: "忘记管理员密码?", forgotAdminPassword: "忘记管理员密码?",
registerLink: "注册", registerLink: "注册",
@@ -761,6 +761,13 @@ export default {
fixedCertExpireDays: "固定证书有效期天数", fixedCertExpireDays: "固定证书有效期天数",
fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示", fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示",
fixedCertExpireDaysRecommend: "推荐90", fixedCertExpireDaysRecommend: "推荐90",
enableOauth: "启用OAuth2登录",
oauthEnabledHelper: "是否启用OAuth2登录",
oauthProviders: "OAuth2登录提供商",
oauthType: "OAuth2登录类型",
oauthConfig: "OAuth2登录配置",
oauthProviderSelectorPlaceholder: "请选择OAuth2登录提供商",
}, },
}, },
modal: { modal: {
@@ -48,28 +48,26 @@
</a-tabs> </a-tabs>
<a-form-item> <a-form-item>
<a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish"> <a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
{{ t("authentication.loginButton") }} {{ queryBindCode ? t("authentication.bindButton") : t("authentication.loginButton") }}
</a-button> </a-button>
<div v-if="!!settingStore.sysPublic.selfServicePasswordRetrievalEnabled && !queryBindCode" class="mt-2"> <div class="mt-2 flex justify-between items-center">
<router-link :to="{ name: 'forgotPassword' }"> <div class="flex items-center gap-2">
{{ t("authentication.forgotPassword") }} <language-toggle class="text-blue-500"></language-toggle>
</router-link> <router-link v-if="!!settingStore.sysPublic.selfServicePasswordRetrievalEnabled && !queryBindCode" :to="{ name: 'forgotPassword' }">
</div> {{ t("authentication.forgotPassword") }}
</a-form-item> </router-link>
</div>
<a-form-item class="user-login-other">
<div class="flex flex-between justify-between items-center">
<language-toggle class="color-blue"></language-toggle>
<router-link v-if="hasRegisterTypeEnabled() && !queryBindCode" class="register" :to="{ name: 'register' }"> <router-link v-if="hasRegisterTypeEnabled() && !queryBindCode" class="register" :to="{ name: 'register' }">
{{ t("authentication.registerLink") }} {{ t("authentication.registerLink") }}
</router-link> </router-link>
</div> </div>
<div class="flex flex-between justify-between items-center mt-5">
<oauth-footer></oauth-footer>
</div>
</a-form-item> </a-form-item>
<div v-if="!queryBindCode" class="w-full">
<oauth-footer></oauth-footer>
</div>
</a-form> </a-form>
<a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout"> <a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout">
<div class="mb-10 flex flex-center">请打开您的Authenticator APP获取动态验证码</div> <div class="mb-10 flex flex-center">请打开您的Authenticator APP获取动态验证码</div>
@@ -12,13 +12,13 @@ export async function OauthLogin(type: string) {
}); });
} }
export async function OauthCallback(type: string, query: Record<string, string>) { export async function OauthToken(type: string, validationCode: string) {
return await request({ return await request({
url: apiPrefix + `/callback`, url: apiPrefix + `/token`,
method: "post", method: "post",
data: { data: {
type, type,
...query, validationCode,
}, },
}); });
} }
@@ -43,3 +43,10 @@ export async function BindUser(code: string) {
}, },
}); });
} }
export async function GetOauthProviders() {
return await request({
url: apiPrefix + "/providers",
method: "post",
});
}
@@ -2,7 +2,8 @@
<div class="oauth-callback-page"> <div class="oauth-callback-page">
<div class="oauth-callback-content"> <div class="oauth-callback-content">
<div v-if="!bindRequired" class="oauth-callback-title"> <div v-if="!bindRequired" class="oauth-callback-title">
<span>登录中...</span> <span v-if="!error">登录中...</span>
<span v-else>{{ error }}</span>
</div> </div>
<div v-else class="oauth-callback-title"> <div v-else class="oauth-callback-title">
<div>第三方登录成功还未绑定账号请选择</div> <div>第三方登录成功还未绑定账号请选择</div>
@@ -29,17 +30,16 @@ import { useUserStore } from "/@/store/user";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const oauthType = route.params.type as string; const oauthType = route.params.type as string;
const validationCode = route.query.validationCode as string;
const query = route.query as Record<string, string>; const error = ref(route.query.error as string);
const userStore = useUserStore(); const userStore = useUserStore();
const bindRequired = ref(false); const bindRequired = ref(false);
const bindCode = ref(""); const bindCode = ref("");
async function handleOauthCallback() { async function handleOauthToken() {
//处理第三方登录回调 //处理第三方登录回调
const res = await api.OauthCallback(oauthType, query); const res = await api.OauthToken(oauthType, validationCode);
if (res.token) { if (res.token) {
//登录成功 //登录成功
userStore.onLoginSuccess(res); userStore.onLoginSuccess(res);
@@ -55,7 +55,10 @@ async function handleOauthCallback() {
} }
onMounted(async () => { onMounted(async () => {
await handleOauthCallback(); if (error.value) {
return;
}
await handleOauthToken();
}); });
async function goBindUser() { async function goBindUser() {
@@ -95,6 +98,7 @@ async function autoRegister() {
width: 500px; width: 500px;
margin: 0 auto; margin: 0 auto;
margin-top: 50px; margin-top: 50px;
margin-bottom: 100px;
.oauth-callback-title { .oauth-callback-title {
font-size: 24px; font-size: 24px;
@@ -1,24 +1,25 @@
<template> <template>
<div class="oauth-footer"> <div class="oauth-footer relative">
<div class="oauth-title">
<div class="oauth-title-text">其他方式登录</div>
</div>
<div v-for="item in oauthList" :key="item.type"> <div v-for="item in oauthList" :key="item.type">
<div class="oauth-icon-button pointer" @click="goOauthLogin(item.type)"> <div class="oauth-icon-button pointer" @click="goOauthLogin(item.name)">
<el-icon :icon="item.icon" /> <div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
<span>{{ item.name }}</span> <div>{{ item.title }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { onMounted, ref } from "vue";
import * as api from "./api"; import * as api from "./api";
const oauthList = ref([ const oauthList = ref([]);
{
name: "OIDC", onMounted(async () => {
type: "oidc", oauthList.value = await api.GetOauthProviders();
icon: "ion:oidc", });
},
]);
async function goOauthLogin(type: string) { async function goOauthLogin(type: string) {
//获取第三方登录URL //获取第三方登录URL
@@ -29,17 +30,56 @@ async function goOauthLogin(type: string) {
</script> </script>
<style lang="less"> <style lang="less">
.oauth-footer { .oauth-footer {
width: 100%;
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
.oauth-title {
width: 100%;
font-size: 14px;
font-weight: 500;
color: #8c8c8c;
position: relative;
.oauth-title-text {
position: relative;
z-index: 1;
text-align: center;
&::after {
content: "";
position: absolute;
top: 50%;
left: 0;
width: 36%;
height: 0.5px;
background-color: #8c8c8c;
}
&::before {
content: "";
position: absolute;
top: 50%;
right: 0;
width: 36%;
height: 0.5px;
background-color: #8c8c8c;
}
}
}
.oauth-icon-button { .oauth-icon-button {
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 16px; padding: 8px 8px;
border-radius: 100px; border-radius: 100px;
.fs-icon {
font-size: 36px;
color: #006be6 !important;
}
} }
} }
</style> </style>
@@ -66,7 +66,7 @@ function onChange(value: string) {
<style lang="less"> <style lang="less">
.page-sys-settings { .page-sys-settings {
.sys-settings-form { .sys-settings-form {
width: 800px; width: 900px;
max-width: 100%; max-width: 100%;
padding: 20px; padding: 20px;
} }
@@ -54,30 +54,34 @@
<div class="helper">{{ t("certd.saveThenTest") }}</div> <div class="helper">{{ t("certd.saveThenTest") }}</div>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item :label="t('certd.enableOauth')" :name="['public', 'oauthEnabled']"> <a-form-item :label="t('certd.sys.setting.enableOauth')" :name="['public', 'oauthEnabled']">
<div class="flex-o"> <div class="flex-o">
<a-switch v-model:checked="formState.public.oauthEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" /> <a-switch v-model:checked="formState.public.oauthEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" />
<vip-button class="ml-5" mode="plus"></vip-button> <vip-button class="ml-5" mode="button"></vip-button>
</div> </div>
</a-form-item> </a-form-item>
<a-form-item v-if="formState.public.oauthEnabled" :label="t('certd.oauthProviders')" :name="['public', 'oauthProviders']"> <a-form-item v-if="formState.public.oauthEnabled" :label="t('certd.sys.setting.oauthProviders')" :name="['public', 'oauthProviders']">
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<table> <table class="w-full table-auto border-collapse border border-gray-400">
<tr> <thead>
<th>{{ t("certd.oauthType") }}</th> <tr>
<th>{{ t("certd.oauthConfig") }}</th> <th class="border border-gray-300 px-4 py-2 w-1/2">{{ t("certd.sys.setting.oauthType") }}</th>
</tr> <th class="border border-gray-300 px-4 py-2 w-1/2">{{ t("certd.sys.setting.oauthConfig") }}</th>
<tr v-for="(item, key) of oauthProviders" :key="key"> </tr>
<td> </thead>
<div class="flex items-center"> <tbody>
<fs-icon :icon="item.icon" /> <tr v-for="(item, key) of oauthProviders" :key="key">
{{ item.title }} <td class="border border-gray-300 px-4 py-2">
</div> <div class="flex items-center" :title="item.desc">
</td> <fs-icon :icon="item.icon" class="mr-2 text-blue-600" />
<td> {{ item.title }}
<AddonSelector v-model:model-value="item.addonId" addon-type="oauth" from="sys" :type="item.name" :placeholder="t('certd.clientIdPlaceholder')" /> </div>
</td> </td>
</tr> <td class="border border-gray-300 px-4 py-2">
<AddonSelector v-model:model-value="item.addonId" addon-type="oauth" from="sys" :type="item.name" :placeholder="t('certd.sys.setting.oauthProviderSelectorPlaceholder')" />
</td>
</tr>
</tbody>
</table> </table>
</div> </div>
</a-form-item> </a-form-item>
@@ -190,7 +194,6 @@ async function loadOauthProviders() {
let list: any = await api.GetOauthProviders(); let list: any = await api.GetOauthProviders();
oauthProviders.value = list; oauthProviders.value = list;
for (const item of list) { for (const item of list) {
debugger;
const type = item.name; const type = item.name;
const provider = formState.public.oauthProviders?.[type]; const provider = formState.public.oauthProviders?.[type];
if (provider) { if (provider) {
@@ -1,12 +1,12 @@
import { addonRegistry, BaseController, Constants, SysInstallInfo, SysSettingsService } from "@certd/lib-server"; import { addonRegistry, BaseController, Constants, SysInstallInfo, SysSettingsService } from "@certd/lib-server";
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core"; import { ALL, Body, Controller, Get, Inject, Param, Post, Provide, Query } from "@midwayjs/core";
import { AddonGetterService } from "../../../modules/pipeline/service/addon-getter-service.js"; import { AddonGetterService } from "../../../modules/pipeline/service/addon-getter-service.js";
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js"; import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
import { LoginService } from "../../../modules/login/service/login-service.js"; import { LoginService } from "../../../modules/login/service/login-service.js";
import { CodeService } from "../../../modules/basic/service/code-service.js"; import { CodeService } from "../../../modules/basic/service/code-service.js";
import { UserService } from "../../../modules/sys/authority/service/user-service.js"; import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { UserEntity } from "../../../modules/sys/authority/entity/user.js"; import { UserEntity } from "../../../modules/sys/authority/entity/user.js";
import { simpleNanoId } from "@certd/basic"; import { logger, simpleNanoId } from "@certd/basic";
import { OauthBoundService } from "../../../modules/login/service/oauth-bound-service.js"; import { OauthBoundService } from "../../../modules/login/service/oauth-bound-service.js";
import { OauthBoundEntity } from "../../../modules/login/entity/oauth-bound.js"; import { OauthBoundEntity } from "../../../modules/login/entity/oauth-bound.js";
@@ -56,33 +56,83 @@ export class ConnectController extends BaseController {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo); const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo?.bindUrl || ""; const bindUrl = installInfo?.bindUrl || "";
//构造登录url //构造登录url
const redirectUrl = `${bindUrl}#/oauth/callback/${body.type}`; const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`;
const loginUrl = await addon.buildLoginUrl({ redirectUri: redirectUrl }); const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl });
return this.ok({loginUrl}); const ticket = this.codeService.setValidationValue(ticketValue)
this.ctx.cookies.set("oauth_ticket", ticket, {
httpOnly: true,
// secure: true,
// sameSite: "strict",
})
return this.ok({ loginUrl, ticket });
} }
@Post('/callback', { summary: Constants.per.guest }) @Get('/callback/:type', { summary: Constants.per.guest })
public async callback(@Body(ALL) body: any) { public async callback(@Param('type') type: string, @Query() query: Record<string, string>) {
//处理登录回调 //处理登录回调
const addon = await this.getOauthProvider(body.type); const addon = await this.getOauthProvider(type);
const tokenRes = await addon.onCallback({ const request = this.ctx.request;
code: body.code, // const ticketValue = this.codeService.getValidationValue(ticket);
state: body.state, // if (!ticketValue) {
}); // throw new Error("登录ticket已过期");
// }
const userInfo = tokenRes.userInfo; const ticket = this.ctx.cookies.get("oauth_ticket");
if (!ticket) {
throw new Error("ticket已过期");
}
const ticketValue = this.codeService.getValidationValue(ticket);
if (!ticketValue) {
throw new Error("ticketValue已过期");
}
const openId = userInfo.openId; const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo?.bindUrl || "";
const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}`
try {
const tokenRes = await addon.onCallback({
code: query.code,
state: query.state,
ticketValue,
currentURL: new URL(currentUrl)
});
const userInfo = tokenRes.userInfo;
const loginRes = await this.loginService.loginByOpenId({ openId, type: body.type });
if (loginRes == null) {
// 用户还未绑定,让用户选择绑定已有账号还是自动注册新账号
const validationCode = await this.codeService.setValidationValue({ const validationCode = await this.codeService.setValidationValue({
type: body.type, type,
userInfo, userInfo,
}); });
const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}`;
this.ctx.redirect(redirectUrl);
} catch (err) {
logger.error(err);
this.ctx.redirect(`${bindUrl}#/oauth/callback/${type}?error=${err.error_description || err.message}`);
}
}
@Post('/token', { summary: Constants.per.guest })
public async token(@Body(ALL) body: { validationCode: string, type: string }) {
const validationValue = await this.codeService.getValidationValue(body.validationCode);
if (!validationValue) {
throw new Error("校验码错误");
}
const type = validationValue.type;
if (type !== body.type) {
throw new Error("校验码错误");
}
const userInfo = validationValue.userInfo;
const openId = userInfo.openId;
const loginRes = await this.loginService.loginByOpenId({ openId, type });
if (loginRes == null) {
return this.ok({ return this.ok({
bindRequired: true, bindRequired: true,
validationCode, validationCode: body.validationCode,
}); });
} }
@@ -98,11 +148,13 @@ export class ConnectController extends BaseController {
if (!validationValue) { if (!validationValue) {
throw new Error("校验码错误"); throw new Error("校验码错误");
} }
const type = validationValue.type;
const userInfo = validationValue.userInfo;
const openId = userInfo.openId;
await this.oauthBoundService.bind({ await this.oauthBoundService.bind({
userId, userId,
type: body.type, type,
openId: validationValue.openId, openId,
}); });
return this.ok(1); return this.ok(1);
} }
@@ -117,12 +169,12 @@ export class ConnectController extends BaseController {
const userInfo = validationValue.userInfo; const userInfo = validationValue.userInfo;
const oauthType = validationValue.type; const oauthType = validationValue.type;
let newUser = new UserEntity() let newUser = new UserEntity()
newUser.username = `${oauthType}:_${userInfo.nickName}_${simpleNanoId(6)}`; newUser.username = `${oauthType}_${userInfo.nickName}_${simpleNanoId(6)}`;
newUser.avatar = userInfo.avatar; newUser.avatar = userInfo.avatar;
newUser.nickName = userInfo.nickName; newUser.nickName = userInfo.nickName || simpleNanoId(6);
newUser = await this.userService.register("username", newUser, async (txManager) => { newUser = await this.userService.register("username", newUser, async (txManager) => {
const oauthBound : OauthBoundEntity = new OauthBoundEntity() const oauthBound: OauthBoundEntity = new OauthBoundEntity()
oauthBound.userId = newUser.id; oauthBound.userId = newUser.id;
oauthBound.type = oauthType; oauthBound.type = oauthType;
oauthBound.openId = userInfo.openId; oauthBound.openId = userInfo.openId;
@@ -225,6 +225,7 @@ export class UserService extends BaseService<UserEntity> {
await this.transaction(async txManager => { await this.transaction(async txManager => {
newUser = await txManager.save(newUser); newUser = await txManager.save(newUser);
user.id = newUser.id;
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser); const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
await txManager.save(userRole); await txManager.save(userRole);
@@ -1,6 +1,8 @@
export type OnCallbackReq = { export type OnCallbackReq = {
code: string; code: string;
state: string; state: string;
currentURL: URL;
ticketValue: any;
} }
export type OauthToken = { export type OauthToken = {
@@ -30,8 +32,12 @@ export type OnBindReply = {
message: string; message: string;
} }
export type LoginUrlReply = {
loginUrl: string;
ticketValue: any;
}
export interface IOauthProvider { export interface IOauthProvider {
buildLoginUrl: (params: { redirectUri: string }) => Promise<string>; buildLoginUrl: (params: { redirectUri: string }) => Promise<LoginUrlReply>;
onCallback: (params: OnCallbackReq) => Promise<OauthToken>; onCallback: (params: OnCallbackReq) => Promise<OauthToken>;
onBind: (params: OnBindReq) => Promise<OnBindReply>;
} }
@@ -1,11 +1,12 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server"; import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { IOauthProvider, OnBindReq, OnCallbackReq } from "../api.js"; import { IOauthProvider, OnCallbackReq } from "../api.js";
@IsAddon({ @IsAddon({
addonType: "oauth", addonType: "oauth",
name: 'oidc', name: 'oidc',
title: 'OpenId connect 认证', title: 'OIDC认证',
desc: '', desc: 'OpenID Connect 认证,统一认证服务',
icon:"simple-icons:fusionauth",
showTest: false, showTest: false,
}) })
export class OidcOauthProvider extends BaseAddon implements IOauthProvider { export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
@@ -54,41 +55,7 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
client client
} }
} }
async onCallback(req: OnCallbackReq) {
const { config, client } = await this.getClient()
const currentUrl = new URL("")
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 }) { async buildLoginUrl(params: { redirectUri: string }) {
const { config, client } = await this.getClient() const { config, client } = await this.getClient()
@@ -126,6 +93,40 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
// now redirect the user to redirectTo.href // now redirect the user to redirectTo.href
console.log('redirecting to', redirectTo.href) console.log('redirecting to', redirectTo.href)
return redirectTo.href; return {
loginUrl: redirectTo.href,
ticketValue: {
codeVerifier: code_verifier,
},
};
} }
async onCallback(req: OnCallbackReq) {
const { config, client } = await this.getClient()
let tokens: any = await client.authorizationCodeGrant(
config,
req.currentURL,
{
expectedState: client.skipStateCheck ,
pkceCodeVerifier: req.ticketValue.codeVerifier,
}
)
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 || claims.preferred_username || "",
avatar: claims.picture,
},
}
};
} }