@better-auth/sso 1.4.7-beta.2 → 1.4.7-beta.4
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-BWvN4yrs.d.mts → index-GoyGoP_a.d.mts} +390 -21
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +559 -63
- package/package.json +4 -4
- package/src/authn-request-store.ts +76 -0
- package/src/authn-request.test.ts +99 -0
- package/src/index.ts +46 -7
- package/src/oidc/discovery.test.ts +823 -0
- package/src/oidc/discovery.ts +355 -0
- package/src/oidc/errors.ts +86 -0
- package/src/oidc/index.ts +31 -0
- package/src/oidc/types.ts +210 -0
- package/src/oidc.test.ts +0 -164
- package/src/routes/sso.ts +415 -96
- package/src/saml.test.ts +781 -48
- package/src/types.ts +81 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.4.7-beta.
|
|
4
|
+
"version": "1.4.7-beta.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@better-fetch/fetch": "1.1.
|
|
55
|
+
"@better-fetch/fetch": "1.1.21",
|
|
56
56
|
"fast-xml-parser": "^5.2.5",
|
|
57
57
|
"jose": "^6.1.0",
|
|
58
58
|
"samlify": "^2.10.1",
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"express": "^5.1.0",
|
|
67
67
|
"oauth2-mock-server": "^8.2.0",
|
|
68
68
|
"tsdown": "^0.17.2",
|
|
69
|
-
"better-auth": "1.4.7-beta.
|
|
69
|
+
"better-auth": "1.4.7-beta.4"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"better-auth": "1.4.7-beta.
|
|
72
|
+
"better-auth": "1.4.7-beta.4"
|
|
73
73
|
},
|
|
74
74
|
"scripts": {
|
|
75
75
|
"test": "vitest",
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { BetterAuthPlugin } from "better-auth";
|
|
2
2
|
import { XMLValidator } from "fast-xml-parser";
|
|
3
3
|
import * as saml from "samlify";
|
|
4
|
+
import type {
|
|
5
|
+
AuthnRequestRecord,
|
|
6
|
+
AuthnRequestStore,
|
|
7
|
+
} from "./authn-request-store";
|
|
8
|
+
import {
|
|
9
|
+
createInMemoryAuthnRequestStore,
|
|
10
|
+
DEFAULT_AUTHN_REQUEST_TTL_MS,
|
|
11
|
+
} from "./authn-request-store";
|
|
4
12
|
import {
|
|
5
13
|
requestDomainVerification,
|
|
6
14
|
verifyDomain,
|
|
@@ -13,9 +21,38 @@ import {
|
|
|
13
21
|
signInSSO,
|
|
14
22
|
spMetadata,
|
|
15
23
|
} from "./routes/sso";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_CLOCK_SKEW_MS,
|
|
27
|
+
type SAMLConditions,
|
|
28
|
+
type TimestampValidationOptions,
|
|
29
|
+
validateSAMLTimestamp,
|
|
30
|
+
} from "./routes/sso";
|
|
31
|
+
|
|
16
32
|
import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "./types";
|
|
17
33
|
|
|
18
34
|
export type { SAMLConfig, OIDCConfig, SSOOptions, SSOProvider };
|
|
35
|
+
export type { AuthnRequestStore, AuthnRequestRecord };
|
|
36
|
+
export { createInMemoryAuthnRequestStore, DEFAULT_AUTHN_REQUEST_TTL_MS };
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
computeDiscoveryUrl,
|
|
40
|
+
type DiscoverOIDCConfigParams,
|
|
41
|
+
DiscoveryError,
|
|
42
|
+
type DiscoveryErrorCode,
|
|
43
|
+
discoverOIDCConfig,
|
|
44
|
+
fetchDiscoveryDocument,
|
|
45
|
+
type HydratedOIDCConfig,
|
|
46
|
+
needsRuntimeDiscovery,
|
|
47
|
+
normalizeDiscoveryUrls,
|
|
48
|
+
normalizeUrl,
|
|
49
|
+
type OIDCDiscoveryDocument,
|
|
50
|
+
REQUIRED_DISCOVERY_FIELDS,
|
|
51
|
+
type RequiredDiscoveryField,
|
|
52
|
+
selectTokenEndpointAuthMethod,
|
|
53
|
+
validateDiscoveryDocument,
|
|
54
|
+
validateDiscoveryUrl,
|
|
55
|
+
} from "./oidc";
|
|
19
56
|
|
|
20
57
|
const fastValidator = {
|
|
21
58
|
async validate(xml: string) {
|
|
@@ -71,19 +108,21 @@ export function sso<O extends SSOOptions>(
|
|
|
71
108
|
};
|
|
72
109
|
|
|
73
110
|
export function sso<O extends SSOOptions>(options?: O | undefined): any {
|
|
111
|
+
const optionsWithStore = options as O;
|
|
112
|
+
|
|
74
113
|
let endpoints = {
|
|
75
114
|
spMetadata: spMetadata(),
|
|
76
|
-
registerSSOProvider: registerSSOProvider(
|
|
77
|
-
signInSSO: signInSSO(
|
|
78
|
-
callbackSSO: callbackSSO(
|
|
79
|
-
callbackSSOSAML: callbackSSOSAML(
|
|
80
|
-
acsEndpoint: acsEndpoint(
|
|
115
|
+
registerSSOProvider: registerSSOProvider(optionsWithStore),
|
|
116
|
+
signInSSO: signInSSO(optionsWithStore),
|
|
117
|
+
callbackSSO: callbackSSO(optionsWithStore),
|
|
118
|
+
callbackSSOSAML: callbackSSOSAML(optionsWithStore),
|
|
119
|
+
acsEndpoint: acsEndpoint(optionsWithStore),
|
|
81
120
|
};
|
|
82
121
|
|
|
83
122
|
if (options?.domainVerification?.enabled) {
|
|
84
123
|
const domainVerificationEndpoints = {
|
|
85
|
-
requestDomainVerification: requestDomainVerification(
|
|
86
|
-
verifyDomain: verifyDomain(
|
|
124
|
+
requestDomainVerification: requestDomainVerification(optionsWithStore),
|
|
125
|
+
verifyDomain: verifyDomain(optionsWithStore),
|
|
87
126
|
};
|
|
88
127
|
|
|
89
128
|
endpoints = {
|