var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
return new (P || (P = Promise))(function (resolve, reject) {
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
});
|
};
|
const BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
/**
|
* Discovers the OpenID Connect provider configuration and returns an authenticated client configuration.
|
*
|
* Uses the `ClientSecretBasic` authentication method and performs
|
* [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)
|
* against the issuer URL.
|
*
|
* @param registration - The client registration details (issuer, credentials, redirect URI).
|
* @param allowInsecureRequests - When `true`, permits plain HTTP requests (useful for local development). Defaults to `false`.
|
* @returns A resolved `Configuration` that can be passed to {@link getAuthenticationAttempt} and {@link callback}.
|
* @throws If discovery fails or the issuer is unreachable.
|
*
|
* @example
|
* ```ts
|
* const config = await getConfig({
|
* issuer: "https://auth.example.com",
|
* clientId: "my-app",
|
* clientSecret: "secret",
|
* redirectUri: "https://my-app.example.com/callback"
|
* });
|
* ```
|
*/
|
export function getConfig(registration_1) {
|
return __awaiter(this, arguments, void 0, function* (registration, allowInsecureRequests = false) {
|
const openId = yield import("openid-client");
|
return openId.discovery(new URL(registration.issuer), registration.clientId, registration.clientSecret, openId.ClientSecretBasic(registration.clientSecret), { execute: allowInsecureRequests ? [openId.allowInsecureRequests] : [] });
|
});
|
}
|
/**
|
* Builds an authorization URL with PKCE and a random state parameter.
|
*
|
* Returns an {@link AuthenticationAttempt} whose `codeVerifier` and `state` must be
|
* persisted in the user's session so they can be validated in the {@link callback} step.
|
*
|
* @param config - The OpenID Connect configuration obtained from {@link getConfig}.
|
* @param redirectUri - The callback URI the provider should redirect to after authentication.
|
* @param scope - Space-separated OAuth 2.0 scopes to request. Defaults to `"openid offline_access profile"`.
|
* @returns An {@link AuthenticationAttempt} containing the authorization URL and PKCE/state values.
|
*
|
* @example
|
* ```ts
|
* const attempt = await getAuthenticationAttempt(config, "https://my-app.example.com/callback");
|
*
|
* // Store in session for later verification
|
* session.codeVerifier = attempt.codeVerifier;
|
* session.state = attempt.state;
|
*
|
* // Redirect the user's browser
|
* res.redirect(attempt.url.toString());
|
* ```
|
*/
|
export function getAuthenticationAttempt(config, redirectUri, scope) {
|
return __awaiter(this, void 0, void 0, function* () {
|
const openId = yield import("openid-client");
|
const codeVerifier = openId.randomPKCECodeVerifier();
|
const codeChallenge = yield openId.calculatePKCECodeChallenge(codeVerifier);
|
const state = openId.randomState();
|
const url = openId.buildAuthorizationUrl(config, {
|
redirect_uri: redirectUri,
|
scope: scope || "openid offline_access profile",
|
code_challenge: codeChallenge,
|
code_challenge_method: "S256",
|
state
|
});
|
return { codeVerifier, codeChallenge, state, url };
|
});
|
}
|
/**
|
* Builds an RP-initiated logout URL for ending the user's session at the identity provider.
|
*
|
* Returns a URL that redirects the user's browser to the provider's end session endpoint,
|
* which will then redirect back to `postLogoutRedirectURI` after the logout completes.
|
*
|
* @param config - The OpenID Connect configuration obtained from {@link getConfig}.
|
* @param postLogoutRedirectURI - The URI the provider should redirect to after the user is logged out.
|
* @param idToken - The `id_token` received during login, passed as `id_token_hint` to identify the session being ended.
|
* @returns A `URL` to redirect the user's browser to in order to trigger logout at the identity provider.
|
*
|
* @example
|
* ```ts
|
* const logoutUrl = await buildLogoutURL(
|
* config,
|
* "https://my-app.example.com/",
|
* session.idToken
|
* );
|
* res.redirect(logoutUrl.toString());
|
* ```
|
*/
|
export function buildLogoutURL(config, postLogoutRedirectURI, idToken) {
|
return __awaiter(this, void 0, void 0, function* () {
|
const openId = yield import("openid-client");
|
return openId.buildEndSessionUrl(config, {
|
post_logout_redirect_uri: postLogoutRedirectURI,
|
id_token_hint: idToken,
|
});
|
});
|
}
|
/**
|
* Handles the OAuth 2.0 authorization code callback.
|
*
|
* Exchanges the authorization code for tokens, decodes the ID token claims,
|
* and fetches additional user information from the UserInfo endpoint.
|
*
|
* @param config - The OpenID Connect configuration obtained from {@link getConfig}.
|
* @param codeVerifier - The PKCE code verifier stored during the {@link getAuthenticationAttempt} step.
|
* @param state - The state value stored during the {@link getAuthenticationAttempt} step.
|
* @param requestUrl - The full callback request URL (including query parameters) from the identity provider redirect.
|
* @returns An {@link AuthenticationResult} with the token set, ID token claims, and user info.
|
*
|
* @example
|
* ```ts
|
* // Inside your /callback route handler:
|
* const requestUrl = new URL(req.url, "https://my-app.example.com");
|
* const result = await callback(config, session.codeVerifier, session.state, requestUrl);
|
*
|
* console.log(result.claims.sub); // unique user identifier
|
* console.log(result.claims.customer_no); // application-specific customer number
|
* console.log(result.userInfo.email); // email from UserInfo endpoint
|
* ```
|
*/
|
export function callback(config, codeVerifier, state, requestUrl) {
|
return __awaiter(this, void 0, void 0, function* () {
|
const openId = yield import("openid-client");
|
const tokenSet = yield openId.authorizationCodeGrant(config, requestUrl, {
|
pkceCodeVerifier: codeVerifier,
|
expectedState: state
|
});
|
const claims = tokenSet.claims();
|
const userInfo = yield openId.fetchUserInfo(config, tokenSet.access_token, claims.sub);
|
return { tokenSet, claims, userInfo };
|
});
|
}
|
/**
|
* Validates an OpenID Connect
|
* [Back-Channel Logout Token](https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation)
|
* received at the relying party's back-channel logout endpoint.
|
*
|
* Verifies the `logout_token` JWT signature against the provider's JWKS (discovered via {@link getConfig}),
|
* enforces `iss` and `aud` match the configured issuer and client, and validates the
|
* logout-specific claim rules:
|
*
|
* - `iat`, `jti`, and `events` are required.
|
* - `events` must contain the `http://schemas.openid.net/event/backchannel-logout` key.
|
* - At least one of `sub` or `sid` must be present.
|
* - `nonce` must NOT be present.
|
*
|
* Replay protection is the caller's responsibility: persist the returned `jti` and reject any
|
* logout_token whose `jti` has already been seen within a reasonable window.
|
*
|
* @param config - The OpenID Connect configuration obtained from {@link getConfig}.
|
* @param logoutToken - The raw `logout_token` JWT posted by the identity provider.
|
* @returns The validated {@link LogoutTokenClaims} (including `sub` and/or `sid`).
|
* @throws If the signature is invalid, issuer/audience do not match, required claims are missing,
|
* the `events` claim lacks the back-channel logout event, or a `nonce` claim is present.
|
*
|
* @example
|
* ```ts
|
* // POST /backchannel-logout
|
* const logoutToken = req.body.logout_token;
|
* const claims = await validateLogoutToken(config, logoutToken);
|
*
|
* if (await seenJti(claims.jti)) return res.status(400).end();
|
* await rememberJti(claims.jti);
|
*
|
* if (claims.sid) await terminateSessionsBySid(claims.sid);
|
* else if (claims.sub) await terminateAllSessionsForUser(claims.sub);
|
*
|
* res.status(200).end();
|
* ```
|
*/
|
export function validateLogoutToken(config, logoutToken) {
|
return __awaiter(this, void 0, void 0, function* () {
|
const jose = yield import("jose");
|
const serverMetadata = config.serverMetadata();
|
if (!serverMetadata.jwks_uri) {
|
throw new Error("Issuer metadata is missing jwks_uri; cannot verify logout_token");
|
}
|
const jwks = jose.createRemoteJWKSet(new URL(serverMetadata.jwks_uri));
|
const { payload } = yield jose.jwtVerify(logoutToken, jwks, {
|
issuer: serverMetadata.issuer,
|
audience: config.clientMetadata().client_id,
|
requiredClaims: ["iat", "jti", "events"]
|
});
|
if ("nonce" in payload) {
|
throw new Error("logout_token MUST NOT contain a nonce claim");
|
}
|
const events = payload.events;
|
if (typeof events !== "object" || events === null || Array.isArray(events) ||
|
!(BACKCHANNEL_LOGOUT_EVENT in events)) {
|
throw new Error(`logout_token events claim must contain "${BACKCHANNEL_LOGOUT_EVENT}"`);
|
}
|
const sub = typeof payload.sub === "string" ? payload.sub : undefined;
|
const sid = typeof payload.sid === "string" ? payload.sid : undefined;
|
if (!sub && !sid) {
|
throw new Error("logout_token must contain at least one of sub or sid");
|
}
|
return {
|
iss: payload.iss,
|
aud: payload.aud,
|
iat: payload.iat,
|
jti: payload.jti,
|
events: events,
|
sub,
|
sid
|
};
|
});
|
}
|