Files
certd/packages/ui/certd-server/src/plugins/plugin-oauth/oidc/plugin-oidc.ts
T

143 lines
3.9 KiB
TypeScript
Raw Normal View History

2025-11-26 23:25:51 +08:00
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
2025-12-01 00:40:46 +08:00
import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
2026-05-13 11:20:55 +08:00
import { IconSets } from "../iconsets.js";
2025-11-26 23:25:51 +08:00
@IsAddon({
addonType: "oauth",
2026-05-31 01:41:33 +08:00
name: "oidc",
title: "OIDC认证",
desc: "OpenID Connect 认证,统一认证服务",
icon: "simple-icons:fusionauth:#006be6",
2025-11-26 23:25:51 +08:00
showTest: false,
})
export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
2025-12-03 00:35:17 +08:00
@AddonInput({
title: "自定义图标",
component: {
2026-05-31 01:41:33 +08:00
name: "fs-icon-selector",
vModel: "modelValue",
2026-05-13 11:20:55 +08:00
iconSets: IconSets,
2025-12-03 00:35:17 +08:00
},
required: false,
})
icon = "";
2025-11-26 23:25:51 +08:00
@AddonInput({
title: "ClientId",
helper: "ClientId / appId",
required: true,
})
clientId = "";
@AddonInput({
title: "ClientSecretKey",
component: {
placeholder: "ClientSecretKey / appSecretKey",
},
required: true,
})
clientSecretKey = "";
@AddonInput({
title: "服务地址",
2025-12-03 00:35:17 +08:00
helper: "Issuer地址,服务发现地址去掉/.well-known/openid-configuration",
2025-11-26 23:25:51 +08:00
component: {
placeholder: "https://oidc.example.com/oidc",
},
required: true,
})
issuerUrl = "";
async getClient() {
const client = await this.importRuntime("openid-client");
2026-05-31 01:41:33 +08:00
const server = new URL(this.issuerUrl); // Authorization Server's Issuer Identifier
2025-11-26 23:25:51 +08:00
2026-05-31 01:41:33 +08:00
const config = await client.discovery(server, this.clientId, this.clientSecretKey);
2025-11-26 23:25:51 +08:00
// console.log(config.serverMetadata())
return {
config,
2026-05-31 01:41:33 +08:00
client,
};
2025-11-26 23:25:51 +08:00
}
2026-05-31 01:41:33 +08:00
2025-11-30 01:13:55 +08:00
async buildLoginUrl(params: BuildLoginUrlReq) {
2026-05-31 01:41:33 +08:00
const { config, client } = await this.getClient();
2025-11-26 23:25:51 +08:00
2026-05-31 01:41:33 +08:00
const redirect_uri = new URL(params.redirectUri);
const scope = "openid profile"; // Scope of the access request
2025-11-26 23:25:51 +08:00
/**
* 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.
*/
2026-05-31 01:41:33 +08:00
const code_verifier = client.randomPKCECodeVerifier();
const code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
let state: any = {
forType: params.forType || "login",
};
state = this.ctx.utils.hash.base64(JSON.stringify(state));
2025-11-26 23:25:51 +08:00
2026-05-31 01:41:33 +08:00
const parameters: any = {
2025-11-26 23:25:51 +08:00
redirect_uri,
scope,
code_challenge,
2026-05-31 01:41:33 +08:00
code_challenge_method: "S256",
2025-11-26 23:25:51 +08:00
state,
2025-12-03 22:00:35 +08:00
nonce: client.randomNonce(),
2026-05-31 01:41:33 +08:00
};
2025-11-26 23:25:51 +08:00
2026-05-31 01:41:33 +08:00
const redirectTo = client.buildAuthorizationUrl(config, parameters);
2025-11-28 01:42:42 +08:00
return {
loginUrl: redirectTo.href,
ticketValue: {
codeVerifier: code_verifier,
state,
2025-12-03 22:00:35 +08:00
nonce: parameters.nonce,
2025-11-28 01:42:42 +08:00
},
};
2025-11-26 23:25:51 +08:00
}
2025-11-28 01:42:42 +08:00
async onCallback(req: OnCallbackReq) {
2026-05-31 01:41:33 +08:00
const { config, client } = await this.getClient();
2025-11-28 01:42:42 +08:00
2026-05-31 01:41:33 +08:00
const tokens: any = await client.authorizationCodeGrant(config, req.currentURL, {
expectedState: req.ticketValue.state,
pkceCodeVerifier: req.ticketValue.codeVerifier,
expectedNonce: req.ticketValue.nonce,
});
const claims = tokens.claims();
2025-11-28 01:42:42 +08:00
return {
2026-05-31 01:41:33 +08:00
token: {
2025-11-28 01:42:42 +08:00
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
},
userInfo: {
openId: claims.sub,
nickName: claims.nickname || claims.name || claims.username || claims.preferred_username || "",
2025-11-28 01:42:42 +08:00
avatar: claims.picture,
2025-12-15 00:23:35 +08:00
email: claims.email || "",
2025-11-28 01:42:42 +08:00
},
2026-05-31 01:41:33 +08:00
};
}
2025-12-01 00:40:46 +08:00
async buildLogoutUrl(params: BuildLogoutUrlReq) {
2026-05-31 01:41:33 +08:00
try {
const { config } = await this.getClient();
const logoutUrl = config.serverMetadata().end_session_endpoint;
return {
logoutUrl: logoutUrl,
};
2026-05-31 01:41:33 +08:00
} catch (err) {
this.logger.error(`获取注销地址失败: ${err}`);
return {
logoutUrl: "",
};
}
2025-12-01 00:40:46 +08:00
}
2025-11-26 23:25:51 +08:00
}