@cat-factory/server 0.223.0 → 0.224.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/oidc/OidcClient.d.ts +86 -0
- package/dist/auth/oidc/OidcClient.d.ts.map +1 -0
- package/dist/auth/oidc/OidcClient.js +271 -0
- package/dist/auth/oidc/OidcClient.js.map +1 -0
- package/dist/auth/oidc/claims.d.ts +97 -0
- package/dist/auth/oidc/claims.d.ts.map +1 -0
- package/dist/auth/oidc/claims.js +149 -0
- package/dist/auth/oidc/claims.js.map +1 -0
- package/dist/auth/oidc/discovery.d.ts +51 -0
- package/dist/auth/oidc/discovery.d.ts.map +1 -0
- package/dist/auth/oidc/discovery.js +165 -0
- package/dist/auth/oidc/discovery.js.map +1 -0
- package/dist/auth/signing.d.ts +9 -0
- package/dist/auth/signing.d.ts.map +1 -1
- package/dist/auth/signing.js +9 -0
- package/dist/auth/signing.js.map +1 -1
- package/dist/config/misconfiguredApp.d.ts.map +1 -1
- package/dist/config/misconfiguredApp.js +14 -1
- package/dist/config/misconfiguredApp.js.map +1 -1
- package/dist/config/problems.js +1 -1
- package/dist/config/problems.js.map +1 -1
- package/dist/config/sso.d.ts +39 -0
- package/dist/config/sso.d.ts.map +1 -0
- package/dist/config/sso.js +169 -0
- package/dist/config/sso.js.map +1 -0
- package/dist/config/types.d.ts +41 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/auth/AuthController.d.ts +0 -18
- package/dist/modules/auth/AuthController.d.ts.map +1 -1
- package/dist/modules/auth/AuthController.js +18 -150
- package/dist/modules/auth/AuthController.js.map +1 -1
- package/dist/modules/auth/loginFlow.d.ts +56 -0
- package/dist/modules/auth/loginFlow.d.ts.map +1 -0
- package/dist/modules/auth/loginFlow.js +162 -0
- package/dist/modules/auth/loginFlow.js.map +1 -0
- package/dist/modules/auth/ssoRoutes.d.ts +4 -0
- package/dist/modules/auth/ssoRoutes.d.ts.map +1 -0
- package/dist/modules/auth/ssoRoutes.js +294 -0
- package/dist/modules/auth/ssoRoutes.js.map +1 -0
- package/dist/modules/localSettings/MothershipConnectController.js +1 -1
- package/dist/modules/localSettings/MothershipConnectController.js.map +1 -1
- package/package.json +9 -8
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Logger, SsoDiscoveryDocument } from '@cat-factory/kernel';
|
|
2
|
+
import type { JWTPayload } from 'jose';
|
|
3
|
+
import type { SsoConfig } from '../../config/types.js';
|
|
4
|
+
import type { OidcProviderDirectory } from './discovery.js';
|
|
5
|
+
/** A failed exchange or verification, carrying the reason the SPA renders copy for. */
|
|
6
|
+
export declare class OidcFlowError extends Error {
|
|
7
|
+
readonly reason: 'exchange_failed' | 'token_invalid';
|
|
8
|
+
constructor(reason: 'exchange_failed' | 'token_invalid', message: string);
|
|
9
|
+
}
|
|
10
|
+
/** The PKCE pair a login leg mints: the secret stays in a cookie, the challenge goes to the IdP. */
|
|
11
|
+
export interface PkcePair {
|
|
12
|
+
verifier: string;
|
|
13
|
+
challenge: string;
|
|
14
|
+
}
|
|
15
|
+
export interface OidcClientDependencies {
|
|
16
|
+
config: SsoConfig;
|
|
17
|
+
directory: OidcProviderDirectory;
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
fetchImpl?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
export declare class OidcClient {
|
|
22
|
+
private readonly deps;
|
|
23
|
+
private readonly log;
|
|
24
|
+
constructor(deps: OidcClientDependencies);
|
|
25
|
+
/** The provider, from the discovery cache. */
|
|
26
|
+
provider(): Promise<SsoDiscoveryDocument>;
|
|
27
|
+
/** Where to send the browser to authenticate. */
|
|
28
|
+
authorizeUrl(params: {
|
|
29
|
+
redirectUri: string;
|
|
30
|
+
state: string;
|
|
31
|
+
nonce: string;
|
|
32
|
+
codeChallenge: string;
|
|
33
|
+
/** Pre-fills the IdP's account picker when the SPA knows who is signing in. Rarely set. */
|
|
34
|
+
loginHint?: string;
|
|
35
|
+
}): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Exchange the callback `code` for tokens. Authenticates as a confidential client
|
|
38
|
+
* (`client_secret_post`, the method every enterprise IdP accepts) AND sends the PKCE verifier,
|
|
39
|
+
* so neither the code alone nor the secret alone is enough.
|
|
40
|
+
*/
|
|
41
|
+
exchangeCode(params: {
|
|
42
|
+
code: string;
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
codeVerifier: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
idToken: string;
|
|
47
|
+
accessToken: string | null;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Verify an ID token and return its claims.
|
|
51
|
+
*
|
|
52
|
+
* On a `kid` the cached key set does not hold, refetches the provider ONCE (the rate-limited
|
|
53
|
+
* key-rotation path) and retries — because a provider rotating its signing keys must cost one
|
|
54
|
+
* fetch, not every login until the cache TTL lapses.
|
|
55
|
+
*
|
|
56
|
+
* Every exit is a payload or an `OidcFlowError`. The unknown-key signal is jose's OWN error and
|
|
57
|
+
* it is useful only INSIDE this method: once the one refetch is spent (or refused by its rate
|
|
58
|
+
* limit) the token is simply unverifiable, and letting the raw error out made the callback leg's
|
|
59
|
+
* `instanceof OidcFlowError` catch miss, which rendered a 500 JSON envelope at a browser
|
|
60
|
+
* mid-redirect instead of the reason the flow promises.
|
|
61
|
+
*/
|
|
62
|
+
verifyIdToken(idToken: string, expected: {
|
|
63
|
+
nonce: string;
|
|
64
|
+
}): Promise<JWTPayload>;
|
|
65
|
+
private verifyAgainst;
|
|
66
|
+
/**
|
|
67
|
+
* Read the userinfo endpoint, or null when the provider exposes none / the call failed.
|
|
68
|
+
*
|
|
69
|
+
* Best-effort BY DESIGN: it supplements the ID token's claims, and a provider that answers
|
|
70
|
+
* slowly must not turn a valid authentication into a failed login. When the missing claim is
|
|
71
|
+
* one admission depends on, the ADMISSION rule refuses (with `email_required` /
|
|
72
|
+
* `group_required`) — so a userinfo outage never silently widens who gets in.
|
|
73
|
+
*/
|
|
74
|
+
fetchUserInfo(accessToken: string): Promise<Record<string, unknown> | null>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Mint a PKCE pair: 32 random bytes as the verifier, its SHA-256 as the S256 challenge.
|
|
78
|
+
*
|
|
79
|
+
* Exported (rather than a private method) because the login leg needs the verifier to put in the
|
|
80
|
+
* cookie and the challenge to put in the redirect, and the callback leg needs neither — keeping
|
|
81
|
+
* it a free function is what stops the secret from being reachable off the client instance.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createPkcePair(): Promise<PkcePair>;
|
|
84
|
+
/** Throw the shared 503 when a caller reached an SSO route on a deployment with none wired. */
|
|
85
|
+
export declare function ssoNotConfigured(): never;
|
|
86
|
+
//# sourceMappingURL=OidcClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OidcClient.d.ts","sourceRoot":"","sources":["../../../src/auth/oidc/OidcClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAGvE,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,MAAM,CAAA;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAgD3D,uFAAuF;AACvF,qBAAa,aAAc,SAAQ,KAAK;IAEpC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,eAAe;IADtD,YACW,MAAM,EAAE,iBAAiB,GAAG,eAAe,EACpD,OAAO,EAAE,MAAM,EAIhB;CACF;AAED,oGAAoG;AACpG,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,SAAS,CAAA;IACjB,SAAS,EAAE,qBAAqB,CAAA;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAED,qBAAa,UAAU;IAGT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,YAA6B,IAAI,EAAE,sBAAsB,EAExD;IAED,8CAA8C;IAC9C,QAAQ,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAExC;IAED,iDAAiD;IAC3C,YAAY,CAAC,MAAM,EAAE;QACzB,WAAW,EAAE,MAAM,CAAA;QACnB,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,aAAa,EAAE,MAAM,CAAA;QACrB,2FAA2F;QAC3F,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAalB;IAED;;;;OAIG;IACG,YAAY,CAAC,MAAM,EAAE;QACzB,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;KACrB,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAsD3D;IAED;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBrF;YAEa,aAAa;IAsC3B;;;;;;;OAOG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAmBhF;CACF;AAmCD;;;;;;GAMG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,QAAQ,CAAC,CAKxD;AAED,+FAA+F;AAC/F,wBAAgB,gBAAgB,IAAI,KAAK,CAKxC"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { UnavailableError, describeError, noopLogger, redactSecrets } from '@cat-factory/kernel';
|
|
2
|
+
import { createLocalJWKSet, jwtVerify } from 'jose';
|
|
3
|
+
import { base64url } from '../../crypto/encoding.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// The OpenID Connect Authorization Code + PKCE client — one adapter for every enterprise IdP.
|
|
6
|
+
//
|
|
7
|
+
// Deliberate choices, each of which is the difference between a login flow and a vulnerability:
|
|
8
|
+
//
|
|
9
|
+
// - **Authorization Code, never implicit.** No token ever crosses the browser's URL bar.
|
|
10
|
+
// - **PKCE (S256) always.** The `code_verifier` lives only in an httpOnly cookie, so an
|
|
11
|
+
// authorization code intercepted from the redirect cannot be exchanged. Sent even to
|
|
12
|
+
// providers whose metadata omits `code_challenge_methods_supported`: an unaware provider
|
|
13
|
+
// ignores the parameters, whereas refusing on the advertisement locks out providers that
|
|
14
|
+
// implement the RFC without announcing it.
|
|
15
|
+
// - **ID token verified against the provider's JWKS, with an ASYMMETRIC algorithm allow-list.**
|
|
16
|
+
// Signature verification is delegated to `jose` rather than hand-rolled: it is Web-Crypto
|
|
17
|
+
// native (so it runs unchanged in a Workers isolate and on Node) and it is the wrong thing to
|
|
18
|
+
// reimplement. The allow-list is what closes the classic JWT holes — `alg: none` and an
|
|
19
|
+
// `HS256` token forged with the deployment's own client secret as the HMAC key.
|
|
20
|
+
// - **Keys are supplied LOCALLY** (`createLocalJWKSet` over the key set our own cache holds)
|
|
21
|
+
// rather than through jose's remote JWKS helper, which keeps its own module-level cache. One
|
|
22
|
+
// cache for the document, and it is the evictable app-cache seam.
|
|
23
|
+
// - **`nonce` is checked here**, bound to the httpOnly cookie the login leg set, so a replayed
|
|
24
|
+
// or injected ID token from another session is refused.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* The signature algorithms an ID token may use. Asymmetric only, so a token can be verified
|
|
28
|
+
* ONLY with a key the provider published — never with a secret this deployment also holds.
|
|
29
|
+
*/
|
|
30
|
+
const ALLOWED_ID_TOKEN_ALGS = [
|
|
31
|
+
'RS256',
|
|
32
|
+
'RS384',
|
|
33
|
+
'RS512',
|
|
34
|
+
'PS256',
|
|
35
|
+
'PS384',
|
|
36
|
+
'PS512',
|
|
37
|
+
'ES256',
|
|
38
|
+
'ES384',
|
|
39
|
+
'ES512',
|
|
40
|
+
'EdDSA',
|
|
41
|
+
];
|
|
42
|
+
/** Clock skew tolerated on `exp`/`iat`/`nbf`. IdP and app clocks routinely differ by seconds. */
|
|
43
|
+
const CLOCK_TOLERANCE = '60s';
|
|
44
|
+
/** The token/userinfo calls sit on a user's critical path — bound them. */
|
|
45
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
46
|
+
/** A failed exchange or verification, carrying the reason the SPA renders copy for. */
|
|
47
|
+
export class OidcFlowError extends Error {
|
|
48
|
+
reason;
|
|
49
|
+
constructor(reason, message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.reason = reason;
|
|
52
|
+
this.name = 'OidcFlowError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class OidcClient {
|
|
56
|
+
deps;
|
|
57
|
+
log;
|
|
58
|
+
constructor(deps) {
|
|
59
|
+
this.deps = deps;
|
|
60
|
+
this.log = deps.logger ?? noopLogger;
|
|
61
|
+
}
|
|
62
|
+
/** The provider, from the discovery cache. */
|
|
63
|
+
provider() {
|
|
64
|
+
return this.deps.directory.resolve(this.deps.config.issuerUrl);
|
|
65
|
+
}
|
|
66
|
+
/** Where to send the browser to authenticate. */
|
|
67
|
+
async authorizeUrl(params) {
|
|
68
|
+
const { metadata } = await this.provider();
|
|
69
|
+
const url = new URL(metadata.authorizationEndpoint);
|
|
70
|
+
url.searchParams.set('client_id', this.deps.config.clientId);
|
|
71
|
+
url.searchParams.set('redirect_uri', params.redirectUri);
|
|
72
|
+
url.searchParams.set('response_type', 'code');
|
|
73
|
+
url.searchParams.set('scope', this.deps.config.scopes);
|
|
74
|
+
url.searchParams.set('state', params.state);
|
|
75
|
+
url.searchParams.set('nonce', params.nonce);
|
|
76
|
+
url.searchParams.set('code_challenge', params.codeChallenge);
|
|
77
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
78
|
+
if (params.loginHint)
|
|
79
|
+
url.searchParams.set('login_hint', params.loginHint);
|
|
80
|
+
return url.toString();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Exchange the callback `code` for tokens. Authenticates as a confidential client
|
|
84
|
+
* (`client_secret_post`, the method every enterprise IdP accepts) AND sends the PKCE verifier,
|
|
85
|
+
* so neither the code alone nor the secret alone is enough.
|
|
86
|
+
*/
|
|
87
|
+
async exchangeCode(params) {
|
|
88
|
+
const { metadata } = await this.provider();
|
|
89
|
+
const doFetch = this.deps.fetchImpl ?? fetch;
|
|
90
|
+
let res;
|
|
91
|
+
try {
|
|
92
|
+
res = await doFetch(metadata.tokenEndpoint, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
accept: 'application/json',
|
|
96
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
97
|
+
},
|
|
98
|
+
body: new URLSearchParams({
|
|
99
|
+
grant_type: 'authorization_code',
|
|
100
|
+
code: params.code,
|
|
101
|
+
redirect_uri: params.redirectUri,
|
|
102
|
+
client_id: this.deps.config.clientId,
|
|
103
|
+
client_secret: this.deps.config.clientSecret,
|
|
104
|
+
code_verifier: params.codeVerifier,
|
|
105
|
+
}),
|
|
106
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new OidcFlowError('exchange_failed', `The token endpoint could not be reached: ${causeText(error)}`);
|
|
111
|
+
}
|
|
112
|
+
const body = (await res.json().catch(() => ({})));
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
// The provider's own `error`/`error_description` is the diagnostic an operator needs
|
|
115
|
+
// ("invalid_client" ⇒ wrong secret, "invalid_grant" ⇒ redirect_uri mismatch), and it is
|
|
116
|
+
// scrubbed because a token-endpoint error routinely echoes the request back.
|
|
117
|
+
const detail = [body.error, body.error_description].filter(Boolean).join(': ');
|
|
118
|
+
throw new OidcFlowError('exchange_failed', `The identity provider refused the authorization code (HTTP ${res.status})` +
|
|
119
|
+
(detail ? `: ${redactSecrets(String(detail)) ?? ''}` : '.'));
|
|
120
|
+
}
|
|
121
|
+
if (typeof body.id_token !== 'string' || body.id_token === '') {
|
|
122
|
+
throw new OidcFlowError('token_invalid', 'The identity provider returned no ID token. Make sure the `openid` scope is granted to this client.');
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
idToken: body.id_token,
|
|
126
|
+
accessToken: typeof body.access_token === 'string' ? body.access_token : null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Verify an ID token and return its claims.
|
|
131
|
+
*
|
|
132
|
+
* On a `kid` the cached key set does not hold, refetches the provider ONCE (the rate-limited
|
|
133
|
+
* key-rotation path) and retries — because a provider rotating its signing keys must cost one
|
|
134
|
+
* fetch, not every login until the cache TTL lapses.
|
|
135
|
+
*
|
|
136
|
+
* Every exit is a payload or an `OidcFlowError`. The unknown-key signal is jose's OWN error and
|
|
137
|
+
* it is useful only INSIDE this method: once the one refetch is spent (or refused by its rate
|
|
138
|
+
* limit) the token is simply unverifiable, and letting the raw error out made the callback leg's
|
|
139
|
+
* `instanceof OidcFlowError` catch miss, which rendered a 500 JSON envelope at a browser
|
|
140
|
+
* mid-redirect instead of the reason the flow promises.
|
|
141
|
+
*/
|
|
142
|
+
async verifyIdToken(idToken, expected) {
|
|
143
|
+
const document = await this.provider();
|
|
144
|
+
try {
|
|
145
|
+
return await this.verifyAgainst(document, idToken, expected);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (!isUnknownKeyError(error))
|
|
149
|
+
throw error;
|
|
150
|
+
const refreshed = await this.deps.directory.refreshForUnknownKey(this.deps.config.issuerUrl, document);
|
|
151
|
+
// Unchanged means the refetch was rate-limited: nothing new to try against.
|
|
152
|
+
if (refreshed === document)
|
|
153
|
+
throw keyNotPublished(error);
|
|
154
|
+
try {
|
|
155
|
+
return await this.verifyAgainst(refreshed, idToken, expected);
|
|
156
|
+
}
|
|
157
|
+
catch (retryError) {
|
|
158
|
+
if (!isUnknownKeyError(retryError))
|
|
159
|
+
throw retryError;
|
|
160
|
+
throw keyNotPublished(retryError);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async verifyAgainst(document, idToken, expected) {
|
|
165
|
+
const keys = createLocalJWKSet(document.jwks);
|
|
166
|
+
let payload;
|
|
167
|
+
try {
|
|
168
|
+
const verified = await jwtVerify(idToken, keys, {
|
|
169
|
+
// The DISCOVERED issuer, never the configured URL: the provider's own spelling is what
|
|
170
|
+
// it signs, and it is what the identity subject is built from.
|
|
171
|
+
issuer: document.metadata.issuer,
|
|
172
|
+
audience: this.deps.config.clientId,
|
|
173
|
+
algorithms: ALLOWED_ID_TOKEN_ALGS,
|
|
174
|
+
clockTolerance: CLOCK_TOLERANCE,
|
|
175
|
+
requiredClaims: ['sub', 'iat', 'exp'],
|
|
176
|
+
});
|
|
177
|
+
payload = verified.payload;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (isUnknownKeyError(error))
|
|
181
|
+
throw error;
|
|
182
|
+
throw new OidcFlowError('token_invalid', `The ID token did not verify: ${causeText(error)}`);
|
|
183
|
+
}
|
|
184
|
+
// `nonce` is not a claim jose checks — it is OIDC's own replay binding, and it is the reason
|
|
185
|
+
// an ID token captured from one user's round-trip cannot be injected into another's.
|
|
186
|
+
if (payload.nonce !== expected.nonce) {
|
|
187
|
+
throw new OidcFlowError('token_invalid', "The ID token's nonce does not match this sign-in attempt.");
|
|
188
|
+
}
|
|
189
|
+
// `azp` identifies which client the token was issued FOR when the audience is multi-valued.
|
|
190
|
+
// Only meaningful when present; when it is, it must be us.
|
|
191
|
+
if (typeof payload.azp === 'string' && payload.azp !== this.deps.config.clientId) {
|
|
192
|
+
throw new OidcFlowError('token_invalid', 'The ID token was issued for a different client.');
|
|
193
|
+
}
|
|
194
|
+
return payload;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Read the userinfo endpoint, or null when the provider exposes none / the call failed.
|
|
198
|
+
*
|
|
199
|
+
* Best-effort BY DESIGN: it supplements the ID token's claims, and a provider that answers
|
|
200
|
+
* slowly must not turn a valid authentication into a failed login. When the missing claim is
|
|
201
|
+
* one admission depends on, the ADMISSION rule refuses (with `email_required` /
|
|
202
|
+
* `group_required`) — so a userinfo outage never silently widens who gets in.
|
|
203
|
+
*/
|
|
204
|
+
async fetchUserInfo(accessToken) {
|
|
205
|
+
const { metadata } = await this.provider();
|
|
206
|
+
if (!metadata.userinfoEndpoint)
|
|
207
|
+
return null;
|
|
208
|
+
const doFetch = this.deps.fetchImpl ?? fetch;
|
|
209
|
+
try {
|
|
210
|
+
const res = await doFetch(metadata.userinfoEndpoint, {
|
|
211
|
+
headers: { accept: 'application/json', authorization: `Bearer ${accessToken}` },
|
|
212
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
213
|
+
});
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
this.log.warn('sso.userinfo.failed', { status: res.status });
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const body = await res.json();
|
|
219
|
+
return body && typeof body === 'object' ? body : null;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
this.log.warn('sso.userinfo.failed', describeError(error));
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** The scrubbed message of a thrown value, as a string safe to interpolate. */
|
|
228
|
+
function causeText(error) {
|
|
229
|
+
return String(describeError(error).err ?? '');
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The refusal for a token whose signing key the provider does not publish, once the single
|
|
233
|
+
* rate-limited refetch has been spent. Reported as `token_invalid` because that is what it is
|
|
234
|
+
* from the caller's side: a token this deployment cannot verify. The message names the key so an
|
|
235
|
+
* operator reading the log can tell a mid-rotation race (retry succeeds) from a provider signing
|
|
236
|
+
* with a key it never published (it does not).
|
|
237
|
+
*/
|
|
238
|
+
function keyNotPublished(error) {
|
|
239
|
+
return new OidcFlowError('token_invalid', 'The ID token is signed with a key the identity provider does not publish in its key set, ' +
|
|
240
|
+
`and refetching the key set did not produce it: ${causeText(error)}`);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Whether a verification failure is "no key with that `kid`" — the key-rotation signal — rather
|
|
244
|
+
* than a bad signature.
|
|
245
|
+
*
|
|
246
|
+
* Matched on jose's stable error `code` rather than its message, so a wording change upstream
|
|
247
|
+
* cannot silently turn a rotation into a hard login failure. `ERR_JWKS_MULTIPLE_MATCHING_KEYS`
|
|
248
|
+
* counts too: an ambiguous match resolves once the stale key is gone.
|
|
249
|
+
*/
|
|
250
|
+
function isUnknownKeyError(error) {
|
|
251
|
+
const code = error?.code;
|
|
252
|
+
return code === 'ERR_JWKS_NO_MATCHING_KEY' || code === 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Mint a PKCE pair: 32 random bytes as the verifier, its SHA-256 as the S256 challenge.
|
|
256
|
+
*
|
|
257
|
+
* Exported (rather than a private method) because the login leg needs the verifier to put in the
|
|
258
|
+
* cookie and the challenge to put in the redirect, and the callback leg needs neither — keeping
|
|
259
|
+
* it a free function is what stops the secret from being reachable off the client instance.
|
|
260
|
+
*/
|
|
261
|
+
export async function createPkcePair() {
|
|
262
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
263
|
+
const verifier = base64url(bytes);
|
|
264
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
265
|
+
return { verifier, challenge: base64url(digest) };
|
|
266
|
+
}
|
|
267
|
+
/** Throw the shared 503 when a caller reached an SSO route on a deployment with none wired. */
|
|
268
|
+
export function ssoNotConfigured() {
|
|
269
|
+
throw new UnavailableError('Single sign-on is not configured on this deployment.', 'sso_not_configured');
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=OidcClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OidcClient.js","sourceRoot":"","sources":["../../../src/auth/oidc/OidcClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SsoErrorReason } from '@cat-factory/contracts';
|
|
2
|
+
import type { SsoConfig } from '../../config/types.js';
|
|
3
|
+
/** The identity an SSO round-trip resolved, normalised across providers. */
|
|
4
|
+
export interface SsoIdentity {
|
|
5
|
+
/** The provider's `sub` — the stable identity, never the email. */
|
|
6
|
+
sub: string;
|
|
7
|
+
email: string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the email may be trusted to LINK this login onto an existing same-email user.
|
|
10
|
+
*
|
|
11
|
+
* `email_verified` is absent from a great many enterprise ID tokens even though the address is
|
|
12
|
+
* authoritative — it comes from the corporate directory, which is the whole trust model an
|
|
13
|
+
* operator adopted this IdP for. So absence is treated as verified and only an EXPLICIT
|
|
14
|
+
* `email_verified: false` is honoured as unverified. The alternative (requiring the claim)
|
|
15
|
+
* silently creates a second user for anyone who also signed in with GitHub, and — because
|
|
16
|
+
* `users.email` is unique and an unverified email is dropped — leaves the SSO account with no
|
|
17
|
+
* email at all, which breaks invitations and every roster display. The residual risk is
|
|
18
|
+
* bounded by the IdP: a directory that lets a user self-assert another person's corporate
|
|
19
|
+
* address has already lost, with or without this claim.
|
|
20
|
+
*/
|
|
21
|
+
emailVerified: boolean;
|
|
22
|
+
name: string | null;
|
|
23
|
+
avatarUrl: string | null;
|
|
24
|
+
/** A display handle: `preferred_username`, else the email, else the `sub`. */
|
|
25
|
+
login: string;
|
|
26
|
+
/** Group memberships read from the configured claim, lowercased. Empty when none released. */
|
|
27
|
+
groups: string[];
|
|
28
|
+
}
|
|
29
|
+
/** Whether a sign-in is admitted, and if not, which rule refused it. */
|
|
30
|
+
export type SsoAdmission = {
|
|
31
|
+
allowed: true;
|
|
32
|
+
} | {
|
|
33
|
+
allowed: false;
|
|
34
|
+
reason: SsoErrorReason;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Read a group-membership claim, tolerating every shape providers ship it in: an array of
|
|
38
|
+
* strings, a single string, a space-separated string (some SAML-to-OIDC bridges), or an array of
|
|
39
|
+
* `{ value }` / `{ name }` objects. Anything unrecognised contributes nothing rather than
|
|
40
|
+
* throwing: one odd entry must not discard the rest of a user's groups.
|
|
41
|
+
*
|
|
42
|
+
* Whether a value is SPLIT on whitespace turns on whether something else already delimited it,
|
|
43
|
+
* and getting that backwards is the silent lockout this whole file warns about. An ARRAY entry is
|
|
44
|
+
* taken WHOLE: Okta, Entra ID and every AD-backed directory permit spaces in a group's name
|
|
45
|
+
* ("Domain Admins" is the canonical one), `AUTH_SSO_REQUIRED_GROUPS` splits on COMMAS so such a
|
|
46
|
+
* name is perfectly addressable, and shredding it into `domain` + `admins` matches nothing an
|
|
47
|
+
* operator can write. Only a BARE STRING is genuinely ambiguous, and there the space-separated
|
|
48
|
+
* spelling is the shape a whole-value read would get wrong.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readGroupClaim(claims: Record<string, unknown>, claimName: string): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Whether a userinfo response describes the SAME subject the verified ID token did.
|
|
53
|
+
*
|
|
54
|
+
* OIDC Core 5.3.2 makes this a MUST, and it is not tidiness: overlaying the ID token's claims
|
|
55
|
+
* LAST already stops `sub` itself from being taken from userinfo, but `email` and `groups` ride
|
|
56
|
+
* the same response and BOTH decide admission. A response for another subject would hand this
|
|
57
|
+
* user that subject's directory groups, which is the one way a `requiredGroups` gate can be
|
|
58
|
+
* satisfied by somebody else's membership. A response with no `sub` at all fails this too: the
|
|
59
|
+
* claim is REQUIRED there, and a merge that cannot be checked is not one to trust.
|
|
60
|
+
*/
|
|
61
|
+
export declare function userinfoMatchesSubject(userinfo: Record<string, unknown>, claims: Record<string, unknown>): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Build the identity from the merged claim bag (ID token, with any userinfo claims overlaid).
|
|
64
|
+
*
|
|
65
|
+
* Returns null when there is no `sub`: without it there is no stable key, and inventing one from
|
|
66
|
+
* the email would key a user's account on a value orgs reassign.
|
|
67
|
+
*/
|
|
68
|
+
export declare function readSsoIdentity(claims: Record<string, unknown>, cfg: Pick<SsoConfig, 'groupsClaim'>): SsoIdentity | null;
|
|
69
|
+
/**
|
|
70
|
+
* Whether this identity may sign in.
|
|
71
|
+
*
|
|
72
|
+
* The default is ADMIT, and that is the feature rather than an oversight: with SSO configured,
|
|
73
|
+
* the identity provider's own app assignment IS the allowlist, which is precisely the capability
|
|
74
|
+
* an org adopts SSO to get (onboarding and — the one that matters — offboarding happen in the
|
|
75
|
+
* directory, not in a list here). Contrast the GitHub login path, which fails closed with both
|
|
76
|
+
* its lists empty: there, nothing else expresses who is allowed.
|
|
77
|
+
*
|
|
78
|
+
* The two optional narrowings are for orgs whose IdP serves more than the population that should
|
|
79
|
+
* reach this deployment, and they are checked in the order whose refusal is most actionable:
|
|
80
|
+
*
|
|
81
|
+
* 1. `requiredGroups` — the user is in none of the named directory groups.
|
|
82
|
+
* 2. `allowedEmailDomains` — their email's domain is not listed. A configured domain gate with
|
|
83
|
+
* NO email released is refused (`email_required`) rather than admitted: admitting would void
|
|
84
|
+
* a rule the operator wrote, and the fix (release the `email` claim) belongs to them.
|
|
85
|
+
*/
|
|
86
|
+
export declare function judgeSsoAdmission(identity: SsoIdentity, cfg: Pick<SsoConfig, 'allowedEmailDomains' | 'requiredGroups'>): SsoAdmission;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the ID token's claims are enough to decide admission and populate a profile, or
|
|
89
|
+
* whether the userinfo endpoint has to be called too.
|
|
90
|
+
*
|
|
91
|
+
* Asked so the extra round-trip happens only when it can change the outcome. Most enterprise
|
|
92
|
+
* providers put `email` in the ID token but release `groups` only from userinfo (Entra ID
|
|
93
|
+
* notably), and one unconditional userinfo call per login is a latency cost paid by every
|
|
94
|
+
* deployment to serve the ones that need it.
|
|
95
|
+
*/
|
|
96
|
+
export declare function needsUserinfo(claims: Record<string, unknown>, cfg: Pick<SsoConfig, 'groupsClaim' | 'requiredGroups' | 'allowedEmailDomains'>): boolean;
|
|
97
|
+
//# sourceMappingURL=claims.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../../../src/auth/oidc/claims.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAiBtD,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,OAAO,CAAA;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAA;IACb,8FAA8F;IAC9F,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,CAAA;AAMzF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAyB3F;AAcD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAGT;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,GAClC,WAAW,GAAG,IAAI,CAcpB;AAUD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,WAAW,EACrB,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,qBAAqB,GAAG,gBAAgB,CAAC,GAC7D,YAAY,CAgBd;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,GAAG,gBAAgB,GAAG,qBAAqB,CAAC,GAC7E,OAAO,CAMT"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
function str(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Read a group-membership claim, tolerating every shape providers ship it in: an array of
|
|
6
|
+
* strings, a single string, a space-separated string (some SAML-to-OIDC bridges), or an array of
|
|
7
|
+
* `{ value }` / `{ name }` objects. Anything unrecognised contributes nothing rather than
|
|
8
|
+
* throwing: one odd entry must not discard the rest of a user's groups.
|
|
9
|
+
*
|
|
10
|
+
* Whether a value is SPLIT on whitespace turns on whether something else already delimited it,
|
|
11
|
+
* and getting that backwards is the silent lockout this whole file warns about. An ARRAY entry is
|
|
12
|
+
* taken WHOLE: Okta, Entra ID and every AD-backed directory permit spaces in a group's name
|
|
13
|
+
* ("Domain Admins" is the canonical one), `AUTH_SSO_REQUIRED_GROUPS` splits on COMMAS so such a
|
|
14
|
+
* name is perfectly addressable, and shredding it into `domain` + `admins` matches nothing an
|
|
15
|
+
* operator can write. Only a BARE STRING is genuinely ambiguous, and there the space-separated
|
|
16
|
+
* spelling is the shape a whole-value read would get wrong.
|
|
17
|
+
*/
|
|
18
|
+
export function readGroupClaim(claims, claimName) {
|
|
19
|
+
if (!claimName)
|
|
20
|
+
return [];
|
|
21
|
+
const raw = claims[claimName];
|
|
22
|
+
if (typeof raw === 'string') {
|
|
23
|
+
return raw
|
|
24
|
+
.split(/\s+/)
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map((part) => part.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
const values = Array.isArray(raw) ? raw : raw === undefined || raw === null ? [] : [raw];
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const entry of values) {
|
|
31
|
+
if (typeof entry === 'string') {
|
|
32
|
+
const value = str(entry);
|
|
33
|
+
if (value)
|
|
34
|
+
out.push(value.toLowerCase());
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (entry && typeof entry === 'object') {
|
|
38
|
+
const nested = str(entry.value ?? entry.name);
|
|
39
|
+
if (nested)
|
|
40
|
+
out.push(nested.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a claim states a NEGATIVE explicitly.
|
|
47
|
+
*
|
|
48
|
+
* `email_verified` is specified as a boolean and its ABSENCE is read as verified (the decision
|
|
49
|
+
* documented on {@link SsoIdentity.emailVerified}), so the only thing this has to catch is a
|
|
50
|
+
* provider that says "no". Some ship JSON booleans as strings, and `"false"` is the one spelling
|
|
51
|
+
* where absence-means-verified would invert the provider's own answer rather than fill a gap.
|
|
52
|
+
*/
|
|
53
|
+
function statesFalse(value) {
|
|
54
|
+
return value === false || value === 'false';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a userinfo response describes the SAME subject the verified ID token did.
|
|
58
|
+
*
|
|
59
|
+
* OIDC Core 5.3.2 makes this a MUST, and it is not tidiness: overlaying the ID token's claims
|
|
60
|
+
* LAST already stops `sub` itself from being taken from userinfo, but `email` and `groups` ride
|
|
61
|
+
* the same response and BOTH decide admission. A response for another subject would hand this
|
|
62
|
+
* user that subject's directory groups, which is the one way a `requiredGroups` gate can be
|
|
63
|
+
* satisfied by somebody else's membership. A response with no `sub` at all fails this too: the
|
|
64
|
+
* claim is REQUIRED there, and a merge that cannot be checked is not one to trust.
|
|
65
|
+
*/
|
|
66
|
+
export function userinfoMatchesSubject(userinfo, claims) {
|
|
67
|
+
const fromUserinfo = str(userinfo.sub);
|
|
68
|
+
return fromUserinfo !== null && fromUserinfo === str(claims.sub);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the identity from the merged claim bag (ID token, with any userinfo claims overlaid).
|
|
72
|
+
*
|
|
73
|
+
* Returns null when there is no `sub`: without it there is no stable key, and inventing one from
|
|
74
|
+
* the email would key a user's account on a value orgs reassign.
|
|
75
|
+
*/
|
|
76
|
+
export function readSsoIdentity(claims, cfg) {
|
|
77
|
+
const sub = str(claims.sub);
|
|
78
|
+
if (!sub)
|
|
79
|
+
return null;
|
|
80
|
+
const email = str(claims.email)?.toLowerCase() ?? null;
|
|
81
|
+
const preferred = str(claims.preferred_username);
|
|
82
|
+
return {
|
|
83
|
+
sub,
|
|
84
|
+
email,
|
|
85
|
+
emailVerified: email !== null && !statesFalse(claims.email_verified),
|
|
86
|
+
name: str(claims.name) ?? joinName(claims),
|
|
87
|
+
avatarUrl: str(claims.picture),
|
|
88
|
+
login: preferred ?? email ?? sub,
|
|
89
|
+
groups: readGroupClaim(claims, cfg.groupsClaim),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** `given_name` + `family_name`, for providers that release no composed `name`. */
|
|
93
|
+
function joinName(claims) {
|
|
94
|
+
const parts = [str(claims.given_name), str(claims.family_name)].filter((part) => part !== null);
|
|
95
|
+
return parts.length > 0 ? parts.join(' ') : null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether this identity may sign in.
|
|
99
|
+
*
|
|
100
|
+
* The default is ADMIT, and that is the feature rather than an oversight: with SSO configured,
|
|
101
|
+
* the identity provider's own app assignment IS the allowlist, which is precisely the capability
|
|
102
|
+
* an org adopts SSO to get (onboarding and — the one that matters — offboarding happen in the
|
|
103
|
+
* directory, not in a list here). Contrast the GitHub login path, which fails closed with both
|
|
104
|
+
* its lists empty: there, nothing else expresses who is allowed.
|
|
105
|
+
*
|
|
106
|
+
* The two optional narrowings are for orgs whose IdP serves more than the population that should
|
|
107
|
+
* reach this deployment, and they are checked in the order whose refusal is most actionable:
|
|
108
|
+
*
|
|
109
|
+
* 1. `requiredGroups` — the user is in none of the named directory groups.
|
|
110
|
+
* 2. `allowedEmailDomains` — their email's domain is not listed. A configured domain gate with
|
|
111
|
+
* NO email released is refused (`email_required`) rather than admitted: admitting would void
|
|
112
|
+
* a rule the operator wrote, and the fix (release the `email` claim) belongs to them.
|
|
113
|
+
*/
|
|
114
|
+
export function judgeSsoAdmission(identity, cfg) {
|
|
115
|
+
if (cfg.requiredGroups.length > 0) {
|
|
116
|
+
const member = identity.groups.some((group) => cfg.requiredGroups.includes(group));
|
|
117
|
+
if (!member)
|
|
118
|
+
return { allowed: false, reason: 'group_required' };
|
|
119
|
+
}
|
|
120
|
+
if (cfg.allowedEmailDomains.length > 0) {
|
|
121
|
+
if (!identity.email || !identity.emailVerified) {
|
|
122
|
+
return { allowed: false, reason: 'email_required' };
|
|
123
|
+
}
|
|
124
|
+
const at = identity.email.lastIndexOf('@');
|
|
125
|
+
const domain = at < 0 ? '' : identity.email.slice(at + 1);
|
|
126
|
+
if (!cfg.allowedEmailDomains.includes(domain)) {
|
|
127
|
+
return { allowed: false, reason: 'domain_not_allowed' };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { allowed: true };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether the ID token's claims are enough to decide admission and populate a profile, or
|
|
134
|
+
* whether the userinfo endpoint has to be called too.
|
|
135
|
+
*
|
|
136
|
+
* Asked so the extra round-trip happens only when it can change the outcome. Most enterprise
|
|
137
|
+
* providers put `email` in the ID token but release `groups` only from userinfo (Entra ID
|
|
138
|
+
* notably), and one unconditional userinfo call per login is a latency cost paid by every
|
|
139
|
+
* deployment to serve the ones that need it.
|
|
140
|
+
*/
|
|
141
|
+
export function needsUserinfo(claims, cfg) {
|
|
142
|
+
if (!str(claims.email))
|
|
143
|
+
return true;
|
|
144
|
+
if (cfg.requiredGroups.length > 0 && readGroupClaim(claims, cfg.groupsClaim).length === 0) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=claims.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims.js","sourceRoot":"","sources":["../../../src/auth/oidc/claims.ts"],"names":[],"mappings":"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"}
|