2025-11-26 23:25:51 +08:00
|
|
|
|
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
|
2025-11-30 01:13:55 +08:00
|
|
|
|
import { BuildLoginUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
|
2025-11-26 23:25:51 +08:00
|
|
|
|
|
|
|
|
|
|
@IsAddon({
|
|
|
|
|
|
addonType: "oauth",
|
|
|
|
|
|
name: 'oidc',
|
2025-11-28 01:42:42 +08:00
|
|
|
|
title: 'OIDC认证',
|
|
|
|
|
|
desc: 'OpenID Connect 认证,统一认证服务',
|
|
|
|
|
|
icon:"simple-icons:fusionauth",
|
2025-11-26 23:25:51 +08:00
|
|
|
|
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: "服务地址",
|
2025-11-29 03:25:21 +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 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-28 01:42:42 +08:00
|
|
|
|
|
2025-11-30 01:13:55 +08:00
|
|
|
|
async buildLoginUrl(params: BuildLoginUrlReq) {
|
2025-11-26 23:25:51 +08:00
|
|
|
|
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)
|
2025-11-29 03:25:21 +08:00
|
|
|
|
let state:any = {
|
|
|
|
|
|
forType: params.forType || 'login',
|
|
|
|
|
|
}
|
|
|
|
|
|
state = this.ctx.utils.hash.base64(JSON.stringify(state))
|
2025-11-26 23:25:51 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2025-11-28 01:42:42 +08:00
|
|
|
|
return {
|
|
|
|
|
|
loginUrl: redirectTo.href,
|
|
|
|
|
|
ticketValue: {
|
|
|
|
|
|
codeVerifier: code_verifier,
|
2025-11-29 03:25:21 +08:00
|
|
|
|
state,
|
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) {
|
|
|
|
|
|
const { config, client } = await this.getClient()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let tokens: any = await client.authorizationCodeGrant(
|
|
|
|
|
|
config,
|
|
|
|
|
|
req.currentURL,
|
|
|
|
|
|
{
|
|
|
|
|
|
expectedState: client.skipStateCheck ,
|
|
|
|
|
|
pkceCodeVerifier: req.ticketValue.codeVerifier,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-11-26 23:25:51 +08:00
|
|
|
|
}
|