@better-auth/sso 1.4.7 → 1.4.8-beta.2
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 +6 -6
- package/dist/client.d.mts +1 -1
- package/dist/{index-B9WMxRdD.d.mts → index-DNWhGQW-.d.mts} +81 -69
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +464 -265
- package/package.json +3 -3
- package/src/constants.ts +42 -0
- package/src/domain-verification.test.ts +1 -0
- package/src/index.ts +39 -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/routes/sso.ts +338 -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 +350 -127
- package/src/types.ts +24 -16
- package/src/authn-request-store.ts +0 -76
- package/src/authn-request.test.ts +0 -99
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OAuth2Tokens, User } from "better-auth";
|
|
2
|
-
import type {
|
|
1
|
+
import type { Awaitable, OAuth2Tokens, User } from "better-auth";
|
|
2
|
+
import type { AlgorithmValidationOptions } from "./saml/algorithms";
|
|
3
3
|
|
|
4
4
|
export interface OIDCMapping {
|
|
5
5
|
id?: string | undefined;
|
|
@@ -121,7 +121,7 @@ export interface SSOOptions {
|
|
|
121
121
|
* The SSO provider
|
|
122
122
|
*/
|
|
123
123
|
provider: SSOProvider<SSOOptions>;
|
|
124
|
-
}) =>
|
|
124
|
+
}) => Awaitable<void>)
|
|
125
125
|
| undefined;
|
|
126
126
|
/**
|
|
127
127
|
* Organization provisioning options
|
|
@@ -222,9 +222,7 @@ export interface SSOOptions {
|
|
|
222
222
|
* ```
|
|
223
223
|
* @default 10
|
|
224
224
|
*/
|
|
225
|
-
providersLimit?:
|
|
226
|
-
| (number | ((user: User) => Promise<number> | number))
|
|
227
|
-
| undefined;
|
|
225
|
+
providersLimit?: (number | ((user: User) => Awaitable<number>)) | undefined;
|
|
228
226
|
/**
|
|
229
227
|
* Trust the email verified flag from the provider.
|
|
230
228
|
*
|
|
@@ -233,7 +231,13 @@ export interface SSOOptions {
|
|
|
233
231
|
*
|
|
234
232
|
* If you want to allow account linking for specific trusted providers, enable the `accountLinking` option in your auth config and specify those
|
|
235
233
|
* providers in the `trustedProviders` list.
|
|
234
|
+
*
|
|
236
235
|
* @default false
|
|
236
|
+
*
|
|
237
|
+
* @deprecated This option is discouraged for new projects. Relying on provider-level `email_verified` is a weaker
|
|
238
|
+
* trust signal compared to using `trustedProviders` in `accountLinking` or enabling `domainVerification` for SSO.
|
|
239
|
+
* Existing configurations will continue to work, but new integrations should use explicit trust mechanisms.
|
|
240
|
+
* This option may be removed in a future major version.
|
|
237
241
|
*/
|
|
238
242
|
trustEmailVerified?: boolean | undefined;
|
|
239
243
|
/**
|
|
@@ -291,16 +295,6 @@ export interface SSOOptions {
|
|
|
291
295
|
* @default 300000 (5 minutes)
|
|
292
296
|
*/
|
|
293
297
|
requestTTL?: number;
|
|
294
|
-
/**
|
|
295
|
-
* Custom AuthnRequest store implementation.
|
|
296
|
-
* Use this to provide a custom storage backend (e.g., Redis-backed store).
|
|
297
|
-
*
|
|
298
|
-
* Providing a custom store automatically enables InResponseTo validation.
|
|
299
|
-
*
|
|
300
|
-
* Note: When not provided, the default storage (secondaryStorage with
|
|
301
|
-
* verification table fallback) is used automatically.
|
|
302
|
-
*/
|
|
303
|
-
authnRequestStore?: AuthnRequestStore;
|
|
304
298
|
/**
|
|
305
299
|
* Clock skew tolerance for SAML assertion timestamp validation in milliseconds.
|
|
306
300
|
* Allows for minor time differences between IdP and SP servers.
|
|
@@ -333,5 +327,19 @@ export interface SSOOptions {
|
|
|
333
327
|
* @default false
|
|
334
328
|
*/
|
|
335
329
|
requireTimestamps?: boolean;
|
|
330
|
+
/**
|
|
331
|
+
* Algorithm validation options for SAML responses.
|
|
332
|
+
*
|
|
333
|
+
* Controls behavior when deprecated algorithms (SHA-1, RSA1_5, 3DES)
|
|
334
|
+
* are detected in SAML responses.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts
|
|
338
|
+
* algorithms: {
|
|
339
|
+
* onDeprecated: "reject" // Reject deprecated algorithms
|
|
340
|
+
* }
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
algorithms?: AlgorithmValidationOptions;
|
|
336
344
|
};
|
|
337
345
|
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AuthnRequest Store
|
|
3
|
-
*
|
|
4
|
-
* Tracks SAML AuthnRequest IDs to enable InResponseTo validation.
|
|
5
|
-
* This prevents:
|
|
6
|
-
* - Unsolicited SAML responses
|
|
7
|
-
* - Cross-provider response injection
|
|
8
|
-
* - Replay attacks
|
|
9
|
-
* - Expired login completions
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export interface AuthnRequestRecord {
|
|
13
|
-
id: string;
|
|
14
|
-
providerId: string;
|
|
15
|
-
createdAt: number;
|
|
16
|
-
expiresAt: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface AuthnRequestStore {
|
|
20
|
-
save(record: AuthnRequestRecord): Promise<void>;
|
|
21
|
-
get(id: string): Promise<AuthnRequestRecord | null>;
|
|
22
|
-
delete(id: string): Promise<void>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Default TTL for AuthnRequest records (5 minutes).
|
|
27
|
-
* This should be sufficient for most IdPs while protecting against stale requests.
|
|
28
|
-
*/
|
|
29
|
-
export const DEFAULT_AUTHN_REQUEST_TTL_MS = 5 * 60 * 1000;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* In-memory implementation of AuthnRequestStore.
|
|
33
|
-
* ⚠️ Only suitable for testing or single-instance non-serverless deployments.
|
|
34
|
-
* For production, rely on the default behavior (uses verification table)
|
|
35
|
-
* or provide a custom Redis-backed store.
|
|
36
|
-
*/
|
|
37
|
-
export function createInMemoryAuthnRequestStore(): AuthnRequestStore {
|
|
38
|
-
const store = new Map<string, AuthnRequestRecord>();
|
|
39
|
-
|
|
40
|
-
const cleanup = () => {
|
|
41
|
-
const now = Date.now();
|
|
42
|
-
for (const [id, record] of store.entries()) {
|
|
43
|
-
if (record.expiresAt < now) {
|
|
44
|
-
store.delete(id);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const cleanupInterval = setInterval(cleanup, 60 * 1000);
|
|
50
|
-
|
|
51
|
-
if (typeof cleanupInterval.unref === "function") {
|
|
52
|
-
cleanupInterval.unref();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
async save(record: AuthnRequestRecord): Promise<void> {
|
|
57
|
-
store.set(record.id, record);
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
async get(id: string): Promise<AuthnRequestRecord | null> {
|
|
61
|
-
const record = store.get(id);
|
|
62
|
-
if (!record) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
if (record.expiresAt < Date.now()) {
|
|
66
|
-
store.delete(id);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
return record;
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
async delete(id: string): Promise<void> {
|
|
73
|
-
store.delete(id);
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createInMemoryAuthnRequestStore,
|
|
4
|
-
DEFAULT_AUTHN_REQUEST_TTL_MS,
|
|
5
|
-
} from "./authn-request-store";
|
|
6
|
-
|
|
7
|
-
describe("AuthnRequest Store", () => {
|
|
8
|
-
describe("In-Memory Store", () => {
|
|
9
|
-
it("should save and retrieve an AuthnRequest record", async () => {
|
|
10
|
-
const store = createInMemoryAuthnRequestStore();
|
|
11
|
-
|
|
12
|
-
const record = {
|
|
13
|
-
id: "_test-request-id-1",
|
|
14
|
-
providerId: "saml-provider-1",
|
|
15
|
-
createdAt: Date.now(),
|
|
16
|
-
expiresAt: Date.now() + DEFAULT_AUTHN_REQUEST_TTL_MS,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
await store.save(record);
|
|
20
|
-
const retrieved = await store.get(record.id);
|
|
21
|
-
|
|
22
|
-
expect(retrieved).toEqual(record);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("should return null for non-existent request ID", async () => {
|
|
26
|
-
const store = createInMemoryAuthnRequestStore();
|
|
27
|
-
|
|
28
|
-
const retrieved = await store.get("_non-existent-id");
|
|
29
|
-
|
|
30
|
-
expect(retrieved).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("should return null for expired request ID", async () => {
|
|
34
|
-
const store = createInMemoryAuthnRequestStore();
|
|
35
|
-
|
|
36
|
-
const record = {
|
|
37
|
-
id: "_expired-request-id",
|
|
38
|
-
providerId: "saml-provider-1",
|
|
39
|
-
createdAt: Date.now() - 10000,
|
|
40
|
-
expiresAt: Date.now() - 1000, // Already expired
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
await store.save(record);
|
|
44
|
-
const retrieved = await store.get(record.id);
|
|
45
|
-
|
|
46
|
-
expect(retrieved).toBeNull();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("should delete a request ID", async () => {
|
|
50
|
-
const store = createInMemoryAuthnRequestStore();
|
|
51
|
-
|
|
52
|
-
const record = {
|
|
53
|
-
id: "_delete-me",
|
|
54
|
-
providerId: "saml-provider-1",
|
|
55
|
-
createdAt: Date.now(),
|
|
56
|
-
expiresAt: Date.now() + DEFAULT_AUTHN_REQUEST_TTL_MS,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
await store.save(record);
|
|
60
|
-
await store.delete(record.id);
|
|
61
|
-
|
|
62
|
-
const retrieved = await store.get(record.id);
|
|
63
|
-
expect(retrieved).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("should handle multiple providers with different request IDs", async () => {
|
|
67
|
-
const store = createInMemoryAuthnRequestStore();
|
|
68
|
-
|
|
69
|
-
const record1 = {
|
|
70
|
-
id: "_request-provider-1",
|
|
71
|
-
providerId: "saml-provider-1",
|
|
72
|
-
createdAt: Date.now(),
|
|
73
|
-
expiresAt: Date.now() + DEFAULT_AUTHN_REQUEST_TTL_MS,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const record2 = {
|
|
77
|
-
id: "_request-provider-2",
|
|
78
|
-
providerId: "saml-provider-2",
|
|
79
|
-
createdAt: Date.now(),
|
|
80
|
-
expiresAt: Date.now() + DEFAULT_AUTHN_REQUEST_TTL_MS,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
await store.save(record1);
|
|
84
|
-
await store.save(record2);
|
|
85
|
-
|
|
86
|
-
const retrieved1 = await store.get(record1.id);
|
|
87
|
-
const retrieved2 = await store.get(record2.id);
|
|
88
|
-
|
|
89
|
-
expect(retrieved1?.providerId).toBe("saml-provider-1");
|
|
90
|
-
expect(retrieved2?.providerId).toBe("saml-provider-2");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("DEFAULT_AUTHN_REQUEST_TTL_MS", () => {
|
|
95
|
-
it("should be 5 minutes in milliseconds", () => {
|
|
96
|
-
expect(DEFAULT_AUTHN_REQUEST_TTL_MS).toBe(5 * 60 * 1000);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
});
|