dh_ackergaul
vor 3 Tagen bb80cdf5a6157ca1f3a276e12e9faae9a4739cb7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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
        };
    });
}