@better-auth/sso 1.4.7-beta.4 → 1.4.8-beta.1
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/.turbo/turbo-build.log +7 -7
- package/dist/client.d.mts +1 -1
- package/dist/{index-GoyGoP_a.d.mts → index-DNWhGQW-.d.mts} +94 -77
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +537 -286
- package/package.json +3 -3
- package/src/constants.ts +42 -0
- package/src/domain-verification.test.ts +1 -0
- package/src/index.ts +38 -11
- package/src/linking/index.ts +2 -0
- package/src/linking/org-assignment.ts +158 -0
- package/src/linking/types.ts +10 -0
- package/src/oidc/discovery.test.ts +359 -25
- package/src/oidc/discovery.ts +168 -29
- package/src/oidc/errors.ts +6 -0
- package/src/oidc/types.ts +9 -0
- package/src/oidc.test.ts +3 -0
- package/src/routes/sso.ts +339 -332
- package/src/saml/algorithms.test.ts +205 -0
- package/src/saml/algorithms.ts +259 -0
- package/src/saml/index.ts +9 -0
- package/src/saml.test.ts +351 -127
- package/src/types.ts +18 -16
- package/src/authn-request-store.ts +0 -76
- package/src/authn-request.test.ts +0 -99
package/src/oidc/discovery.ts
CHANGED
|
@@ -24,14 +24,15 @@ const DEFAULT_DISCOVERY_TIMEOUT = 10000;
|
|
|
24
24
|
*
|
|
25
25
|
* This function:
|
|
26
26
|
* 1. Computes the discovery URL from the issuer
|
|
27
|
-
* 2. Validates the discovery URL
|
|
27
|
+
* 2. Validates the discovery URL
|
|
28
28
|
* 3. Fetches the discovery document
|
|
29
29
|
* 4. Validates the discovery document (issuer match + required fields)
|
|
30
|
-
* 5. Normalizes URLs
|
|
30
|
+
* 5. Normalizes URLs
|
|
31
31
|
* 6. Selects token endpoint auth method
|
|
32
32
|
* 7. Merges with existing config (existing values take precedence)
|
|
33
33
|
*
|
|
34
34
|
* @param params - Discovery parameters
|
|
35
|
+
* @param isTrustedOrigin - Origin verification tester function
|
|
35
36
|
* @returns Hydrated OIDC configuration ready for persistence
|
|
36
37
|
* @throws DiscoveryError on any failure
|
|
37
38
|
*/
|
|
@@ -49,13 +50,17 @@ export async function discoverOIDCConfig(
|
|
|
49
50
|
existingConfig?.discoveryEndpoint ||
|
|
50
51
|
computeDiscoveryUrl(issuer);
|
|
51
52
|
|
|
52
|
-
validateDiscoveryUrl(discoveryUrl);
|
|
53
|
+
validateDiscoveryUrl(discoveryUrl, params.isTrustedOrigin);
|
|
53
54
|
|
|
54
55
|
const discoveryDoc = await fetchDiscoveryDocument(discoveryUrl, timeout);
|
|
55
56
|
|
|
56
57
|
validateDiscoveryDocument(discoveryDoc, issuer);
|
|
57
58
|
|
|
58
|
-
const normalizedDoc = normalizeDiscoveryUrls(
|
|
59
|
+
const normalizedDoc = normalizeDiscoveryUrls(
|
|
60
|
+
discoveryDoc,
|
|
61
|
+
issuer,
|
|
62
|
+
params.isTrustedOrigin,
|
|
63
|
+
);
|
|
59
64
|
|
|
60
65
|
const tokenEndpointAuth = selectTokenEndpointAuthMethod(
|
|
61
66
|
normalizedDoc,
|
|
@@ -99,27 +104,20 @@ export function computeDiscoveryUrl(issuer: string): string {
|
|
|
99
104
|
* Validate a discovery URL before fetching.
|
|
100
105
|
*
|
|
101
106
|
* @param url - The discovery URL to validate
|
|
107
|
+
* @param isTrustedOrigin - Origin verification tester function
|
|
102
108
|
* @throws DiscoveryError if URL is invalid
|
|
103
109
|
*/
|
|
104
|
-
export function validateDiscoveryUrl(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{ url, protocol: parsed.protocol },
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
} catch (error) {
|
|
115
|
-
if (error instanceof DiscoveryError) {
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
110
|
+
export function validateDiscoveryUrl(
|
|
111
|
+
url: string,
|
|
112
|
+
isTrustedOrigin: DiscoverOIDCConfigParams["isTrustedOrigin"],
|
|
113
|
+
): void {
|
|
114
|
+
const discoveryEndpoint = parseURL("discoveryEndpoint", url).toString();
|
|
115
|
+
|
|
116
|
+
if (!isTrustedOrigin(discoveryEndpoint)) {
|
|
118
117
|
throw new DiscoveryError(
|
|
119
|
-
"
|
|
120
|
-
`
|
|
121
|
-
{ url },
|
|
122
|
-
{ cause: error },
|
|
118
|
+
"discovery_untrusted_origin",
|
|
119
|
+
`The main discovery endpoint "${discoveryEndpoint}" is not trusted by your trusted origins configuration.`,
|
|
120
|
+
{ url: discoveryEndpoint },
|
|
123
121
|
);
|
|
124
122
|
}
|
|
125
123
|
}
|
|
@@ -276,26 +274,167 @@ export function validateDiscoveryDocument(
|
|
|
276
274
|
/**
|
|
277
275
|
* Normalize URLs in the discovery document.
|
|
278
276
|
*
|
|
279
|
-
* @param
|
|
280
|
-
* @param
|
|
277
|
+
* @param document - The discovery document
|
|
278
|
+
* @param issuer - The base issuer URL
|
|
279
|
+
* @param isTrustedOrigin - Origin verification tester function
|
|
281
280
|
* @returns The normalized discovery document
|
|
282
281
|
*/
|
|
283
282
|
export function normalizeDiscoveryUrls(
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
document: OIDCDiscoveryDocument,
|
|
284
|
+
issuer: string,
|
|
285
|
+
isTrustedOrigin: DiscoverOIDCConfigParams["isTrustedOrigin"],
|
|
286
286
|
): OIDCDiscoveryDocument {
|
|
287
|
+
const doc = { ...document };
|
|
288
|
+
|
|
289
|
+
doc.token_endpoint = normalizeAndValidateUrl(
|
|
290
|
+
"token_endpoint",
|
|
291
|
+
doc.token_endpoint,
|
|
292
|
+
issuer,
|
|
293
|
+
isTrustedOrigin,
|
|
294
|
+
);
|
|
295
|
+
doc.authorization_endpoint = normalizeAndValidateUrl(
|
|
296
|
+
"authorization_endpoint",
|
|
297
|
+
doc.authorization_endpoint,
|
|
298
|
+
issuer,
|
|
299
|
+
isTrustedOrigin,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
doc.jwks_uri = normalizeAndValidateUrl(
|
|
303
|
+
"jwks_uri",
|
|
304
|
+
doc.jwks_uri,
|
|
305
|
+
issuer,
|
|
306
|
+
isTrustedOrigin,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (doc.userinfo_endpoint) {
|
|
310
|
+
doc.userinfo_endpoint = normalizeAndValidateUrl(
|
|
311
|
+
"userinfo_endpoint",
|
|
312
|
+
doc.userinfo_endpoint,
|
|
313
|
+
issuer,
|
|
314
|
+
isTrustedOrigin,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (doc.revocation_endpoint) {
|
|
319
|
+
doc.revocation_endpoint = normalizeAndValidateUrl(
|
|
320
|
+
"revocation_endpoint",
|
|
321
|
+
doc.revocation_endpoint,
|
|
322
|
+
issuer,
|
|
323
|
+
isTrustedOrigin,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (doc.end_session_endpoint) {
|
|
328
|
+
doc.end_session_endpoint = normalizeAndValidateUrl(
|
|
329
|
+
"end_session_endpoint",
|
|
330
|
+
doc.end_session_endpoint,
|
|
331
|
+
issuer,
|
|
332
|
+
isTrustedOrigin,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (doc.introspection_endpoint) {
|
|
337
|
+
doc.introspection_endpoint = normalizeAndValidateUrl(
|
|
338
|
+
"introspection_endpoint",
|
|
339
|
+
doc.introspection_endpoint,
|
|
340
|
+
issuer,
|
|
341
|
+
isTrustedOrigin,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
287
345
|
return doc;
|
|
288
346
|
}
|
|
289
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Normalizes and validates a single URL endpoint
|
|
350
|
+
* @param name The url name
|
|
351
|
+
* @param endpoint The url to validate
|
|
352
|
+
* @param issuer The issuer base url
|
|
353
|
+
* @param isTrustedOrigin - Origin verification tester function
|
|
354
|
+
* @returns
|
|
355
|
+
*/
|
|
356
|
+
function normalizeAndValidateUrl(
|
|
357
|
+
name: string,
|
|
358
|
+
endpoint: string,
|
|
359
|
+
issuer: string,
|
|
360
|
+
isTrustedOrigin: DiscoverOIDCConfigParams["isTrustedOrigin"],
|
|
361
|
+
): string {
|
|
362
|
+
const url = normalizeUrl(name, endpoint, issuer);
|
|
363
|
+
|
|
364
|
+
if (!isTrustedOrigin(url)) {
|
|
365
|
+
throw new DiscoveryError(
|
|
366
|
+
"discovery_untrusted_origin",
|
|
367
|
+
`The ${name} "${url}" is not trusted by your trusted origins configuration.`,
|
|
368
|
+
{ endpoint: name, url },
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return url;
|
|
373
|
+
}
|
|
374
|
+
|
|
290
375
|
/**
|
|
291
376
|
* Normalize a single URL endpoint.
|
|
292
377
|
*
|
|
378
|
+
* @param name - The endpoint name (e.g token_endpoint)
|
|
293
379
|
* @param endpoint - The endpoint URL to normalize
|
|
294
|
-
* @param
|
|
380
|
+
* @param issuer - The base issuer URL
|
|
295
381
|
* @returns The normalized endpoint URL
|
|
296
382
|
*/
|
|
297
|
-
export function normalizeUrl(
|
|
298
|
-
|
|
383
|
+
export function normalizeUrl(
|
|
384
|
+
name: string,
|
|
385
|
+
endpoint: string,
|
|
386
|
+
issuer: string,
|
|
387
|
+
): string {
|
|
388
|
+
try {
|
|
389
|
+
return parseURL(name, endpoint).toString();
|
|
390
|
+
} catch {
|
|
391
|
+
// In case of error, endpoint maybe a relative url
|
|
392
|
+
// So we try to resolve it relative to the issuer
|
|
393
|
+
|
|
394
|
+
const issuerURL = parseURL(name, issuer);
|
|
395
|
+
const basePath = issuerURL.pathname.replace(/\/+$/, "");
|
|
396
|
+
const endpointPath = endpoint.replace(/^\/+/, "");
|
|
397
|
+
|
|
398
|
+
return parseURL(
|
|
399
|
+
name,
|
|
400
|
+
basePath + "/" + endpointPath,
|
|
401
|
+
issuerURL.origin,
|
|
402
|
+
).toString();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Parses the given URL or throws in case of invalid or unsupported protocols
|
|
408
|
+
*
|
|
409
|
+
* @param name the url name
|
|
410
|
+
* @param endpoint the endpoint url
|
|
411
|
+
* @param [base] optional base path
|
|
412
|
+
* @returns
|
|
413
|
+
*/
|
|
414
|
+
function parseURL(name: string, endpoint: string, base?: string) {
|
|
415
|
+
let endpointURL: URL | undefined;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
endpointURL = new URL(endpoint, base);
|
|
419
|
+
if (endpointURL.protocol === "http:" || endpointURL.protocol === "https:") {
|
|
420
|
+
return endpointURL;
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
throw new DiscoveryError(
|
|
424
|
+
"discovery_invalid_url",
|
|
425
|
+
`The url "${name}" must be valid: ${endpoint}`,
|
|
426
|
+
{
|
|
427
|
+
url: endpoint,
|
|
428
|
+
},
|
|
429
|
+
{ cause: error },
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
throw new DiscoveryError(
|
|
434
|
+
"discovery_invalid_url",
|
|
435
|
+
`The url "${name}" must use the http or https supported protocols: ${endpoint}`,
|
|
436
|
+
{ url: endpoint, protocol: endpointURL.protocol },
|
|
437
|
+
);
|
|
299
438
|
}
|
|
300
439
|
|
|
301
440
|
/**
|
package/src/oidc/errors.ts
CHANGED
|
@@ -50,6 +50,12 @@ export function mapDiscoveryErrorToAPIError(error: DiscoveryError): APIError {
|
|
|
50
50
|
code: error.code,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
case "discovery_untrusted_origin":
|
|
54
|
+
return new APIError("BAD_REQUEST", {
|
|
55
|
+
message: `Untrusted OIDC discovery URL: ${error.message}`,
|
|
56
|
+
code: error.code,
|
|
57
|
+
});
|
|
58
|
+
|
|
53
59
|
case "discovery_invalid_json":
|
|
54
60
|
return new APIError("BAD_REQUEST", {
|
|
55
61
|
message: `OIDC discovery returned invalid data: ${error.message}`,
|
package/src/oidc/types.ts
CHANGED
|
@@ -103,6 +103,8 @@ export type DiscoveryErrorCode =
|
|
|
103
103
|
| "discovery_invalid_json"
|
|
104
104
|
/** Discovery URL is invalid or malformed */
|
|
105
105
|
| "discovery_invalid_url"
|
|
106
|
+
/** Discovery URL is not trusted by the trusted origins configuration */
|
|
107
|
+
| "discovery_untrusted_origin"
|
|
106
108
|
/** Discovery document issuer doesn't match configured issuer */
|
|
107
109
|
| "issuer_mismatch"
|
|
108
110
|
/** Discovery document is missing required fields */
|
|
@@ -195,6 +197,13 @@ export interface DiscoverOIDCConfigParams {
|
|
|
195
197
|
* @default 10000 (10 seconds)
|
|
196
198
|
*/
|
|
197
199
|
timeout?: number;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Trusted origin predicate. See "trustedOrigins" option
|
|
203
|
+
* @param url the url to test
|
|
204
|
+
* @returns {boolean} return true for urls that belong to a trusted origin and false otherwise
|
|
205
|
+
*/
|
|
206
|
+
isTrustedOrigin: (url: string) => boolean;
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
/**
|
package/src/oidc.test.ts
CHANGED
|
@@ -12,6 +12,7 @@ let server = new OAuth2Server();
|
|
|
12
12
|
describe("SSO", async () => {
|
|
13
13
|
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
|
|
14
14
|
await getTestInstance({
|
|
15
|
+
trustedOrigins: ["http://localhost:8080"],
|
|
15
16
|
plugins: [sso(), organization()],
|
|
16
17
|
});
|
|
17
18
|
|
|
@@ -257,6 +258,7 @@ describe("SSO", async () => {
|
|
|
257
258
|
describe("SSO disable implicit sign in", async () => {
|
|
258
259
|
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
|
|
259
260
|
await getTestInstance({
|
|
261
|
+
trustedOrigins: ["http://localhost:8080"],
|
|
260
262
|
plugins: [sso({ disableImplicitSignUp: true }), organization()],
|
|
261
263
|
});
|
|
262
264
|
|
|
@@ -419,6 +421,7 @@ describe("SSO disable implicit sign in", async () => {
|
|
|
419
421
|
describe("provisioning", async (ctx) => {
|
|
420
422
|
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
|
|
421
423
|
await getTestInstance({
|
|
424
|
+
trustedOrigins: ["http://localhost:8080"],
|
|
422
425
|
plugins: [sso(), organization()],
|
|
423
426
|
});
|
|
424
427
|
|