@better-auth/sso 1.4.18 → 1.4.19
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-C4nbdf2g.d.mts → index-D-VInsst.d.mts} +7 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +97 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/domain-verification.test.ts +46 -4
- package/src/linking/org-assignment.ts +2 -2
- package/src/oidc.test.ts +1 -3
- package/src/routes/domain-verification.ts +34 -12
- package/src/routes/sso.ts +131 -93
- package/src/saml-state.ts +1 -1
- package/src/saml.test.ts +392 -0
- package/src/types.ts +6 -2
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.
|
|
4
|
+
"version": "1.4.19",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -67,11 +67,12 @@
|
|
|
67
67
|
"express": "^5.1.0",
|
|
68
68
|
"oauth2-mock-server": "^8.2.0",
|
|
69
69
|
"tsdown": "^0.17.2",
|
|
70
|
-
"better-auth": "1.4.
|
|
70
|
+
"better-auth": "1.4.19"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"@better-auth/utils": "0.3.0",
|
|
74
|
-
"better-
|
|
74
|
+
"better-call": "1.1.8",
|
|
75
|
+
"better-auth": "1.4.19"
|
|
75
76
|
},
|
|
76
77
|
"scripts": {
|
|
77
78
|
"test": "vitest",
|
|
@@ -286,7 +286,7 @@ describe("Domain verification", async () => {
|
|
|
286
286
|
|
|
287
287
|
dnsMock.resolveTxt.mockResolvedValue([
|
|
288
288
|
[
|
|
289
|
-
`
|
|
289
|
+
`_better-auth-token-saml-provider-1=${provider.domainVerificationToken}`,
|
|
290
290
|
],
|
|
291
291
|
]);
|
|
292
292
|
|
|
@@ -471,7 +471,7 @@ describe("Domain verification", async () => {
|
|
|
471
471
|
"v=spf1 ip4:50.242.118.232/29 include:_spf.google.com include:mail.zendesk.com ~all",
|
|
472
472
|
],
|
|
473
473
|
[
|
|
474
|
-
`
|
|
474
|
+
`_better-auth-token-saml-provider-1=${provider.domainVerificationToken}`,
|
|
475
475
|
],
|
|
476
476
|
]);
|
|
477
477
|
|
|
@@ -484,6 +484,9 @@ describe("Domain verification", async () => {
|
|
|
484
484
|
});
|
|
485
485
|
|
|
486
486
|
expect(response.status).toBe(204);
|
|
487
|
+
expect(dnsMock.resolveTxt).toHaveBeenCalledWith(
|
|
488
|
+
"_better-auth-token-saml-provider-1.hello.com",
|
|
489
|
+
);
|
|
487
490
|
});
|
|
488
491
|
|
|
489
492
|
it("should verify a provider domain ownership (custom token verification prefix)", async () => {
|
|
@@ -498,7 +501,7 @@ describe("Domain verification", async () => {
|
|
|
498
501
|
[
|
|
499
502
|
"v=spf1 ip4:50.242.118.232/29 include:_spf.google.com include:mail.zendesk.com ~all",
|
|
500
503
|
],
|
|
501
|
-
[`
|
|
504
|
+
[`_auth-prefix-saml-provider-1=${provider.domainVerificationToken}`],
|
|
502
505
|
]);
|
|
503
506
|
|
|
504
507
|
const response = await auth.api.verifyDomain({
|
|
@@ -510,6 +513,45 @@ describe("Domain verification", async () => {
|
|
|
510
513
|
});
|
|
511
514
|
|
|
512
515
|
expect(response.status).toBe(204);
|
|
516
|
+
expect(dnsMock.resolveTxt).toHaveBeenCalledWith(
|
|
517
|
+
"_auth-prefix-saml-provider-1.hello.com",
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("should return bad request when provider ID exceeds DNS label limit", async () => {
|
|
522
|
+
const longProviderId = "a".repeat(50);
|
|
523
|
+
const { auth, getAuthHeaders } = createTestAuth();
|
|
524
|
+
const headers = await getAuthHeaders(testUser);
|
|
525
|
+
|
|
526
|
+
await auth.api.registerSSOProvider({
|
|
527
|
+
body: {
|
|
528
|
+
providerId: longProviderId,
|
|
529
|
+
issuer: "http://hello.com:8081",
|
|
530
|
+
domain: "http://hello.com:8081",
|
|
531
|
+
samlConfig: {
|
|
532
|
+
entryPoint: "http://idp.com:",
|
|
533
|
+
cert: "the-cert",
|
|
534
|
+
callbackUrl: "http://hello.com:8081/api/sso/saml2/callback",
|
|
535
|
+
spMetadata: {},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
headers,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const response = await auth.api.verifyDomain({
|
|
542
|
+
body: {
|
|
543
|
+
providerId: longProviderId,
|
|
544
|
+
},
|
|
545
|
+
headers,
|
|
546
|
+
asResponse: true,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(response.status).toBe(400);
|
|
550
|
+
expect(await response.json()).toEqual({
|
|
551
|
+
message:
|
|
552
|
+
"Verification identifier exceeds the DNS label limit of 63 characters",
|
|
553
|
+
code: "IDENTIFIER_TOO_LONG",
|
|
554
|
+
});
|
|
513
555
|
});
|
|
514
556
|
|
|
515
557
|
it("should fail to verify an already verified domain", async () => {
|
|
@@ -519,7 +561,7 @@ describe("Domain verification", async () => {
|
|
|
519
561
|
|
|
520
562
|
dnsMock.resolveTxt.mockResolvedValue([
|
|
521
563
|
[
|
|
522
|
-
`
|
|
564
|
+
`_better-auth-token-saml-provider-1=${provider.domainVerificationToken}`,
|
|
523
565
|
],
|
|
524
566
|
]);
|
|
525
567
|
|
|
@@ -5,13 +5,13 @@ import type { NormalizedSSOProfile } from "./types";
|
|
|
5
5
|
|
|
6
6
|
export interface OrganizationProvisioningOptions {
|
|
7
7
|
disabled?: boolean;
|
|
8
|
-
defaultRole?:
|
|
8
|
+
defaultRole?: string;
|
|
9
9
|
getRole?: (data: {
|
|
10
10
|
user: User & Record<string, any>;
|
|
11
11
|
userInfo: Record<string, any>;
|
|
12
12
|
token?: OAuth2Tokens;
|
|
13
13
|
provider: SSOProvider<SSOOptions>;
|
|
14
|
-
}) => Promise<
|
|
14
|
+
}) => Promise<string>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface AssignOrganizationFromProviderOptions {
|
package/src/oidc.test.ts
CHANGED
|
@@ -393,9 +393,7 @@ describe("SSO disable implicit sign in", async () => {
|
|
|
393
393
|
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
|
|
394
394
|
);
|
|
395
395
|
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
|
|
396
|
-
expect(callbackURL).toContain(
|
|
397
|
-
"/api/auth/error/error?error=signup disabled",
|
|
398
|
-
);
|
|
396
|
+
expect(callbackURL).toContain("/api/auth/error?error=signup disabled");
|
|
399
397
|
});
|
|
400
398
|
|
|
401
399
|
it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
|
|
@@ -8,10 +8,22 @@ import { generateRandomString } from "better-auth/crypto";
|
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
9
|
import type { SSOOptions, SSOProvider } from "../types";
|
|
10
10
|
|
|
11
|
+
const DNS_LABEL_MAX_LENGTH = 63;
|
|
12
|
+
const DEFAULT_TOKEN_PREFIX = "better-auth-token";
|
|
13
|
+
|
|
11
14
|
const domainVerificationBodySchema = z.object({
|
|
12
15
|
providerId: z.string(),
|
|
13
16
|
});
|
|
14
17
|
|
|
18
|
+
export function getVerificationIdentifier(
|
|
19
|
+
options: SSOOptions,
|
|
20
|
+
providerId: string,
|
|
21
|
+
): string {
|
|
22
|
+
const tokenPrefix =
|
|
23
|
+
options.domainVerification?.tokenPrefix || DEFAULT_TOKEN_PREFIX;
|
|
24
|
+
return `_${tokenPrefix}-${providerId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
export const requestDomainVerification = (options: SSOOptions) => {
|
|
16
28
|
return createAuthEndpoint(
|
|
17
29
|
"/sso/request-domain-verification",
|
|
@@ -83,15 +95,18 @@ export const requestDomainVerification = (options: SSOOptions) => {
|
|
|
83
95
|
});
|
|
84
96
|
}
|
|
85
97
|
|
|
98
|
+
const identifier = getVerificationIdentifier(
|
|
99
|
+
options,
|
|
100
|
+
provider.providerId,
|
|
101
|
+
);
|
|
102
|
+
|
|
86
103
|
const activeVerification =
|
|
87
104
|
await ctx.context.adapter.findOne<Verification>({
|
|
88
105
|
model: "verification",
|
|
89
106
|
where: [
|
|
90
107
|
{
|
|
91
108
|
field: "identifier",
|
|
92
|
-
value:
|
|
93
|
-
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
94
|
-
: `better-auth-token-${provider.providerId}`,
|
|
109
|
+
value: identifier,
|
|
95
110
|
},
|
|
96
111
|
{ field: "expiresAt", value: new Date(), operator: "gt" },
|
|
97
112
|
],
|
|
@@ -106,9 +121,7 @@ export const requestDomainVerification = (options: SSOOptions) => {
|
|
|
106
121
|
await ctx.context.adapter.create<Verification>({
|
|
107
122
|
model: "verification",
|
|
108
123
|
data: {
|
|
109
|
-
identifier
|
|
110
|
-
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
111
|
-
: `better-auth-token-${provider.providerId}`,
|
|
124
|
+
identifier,
|
|
112
125
|
createdAt: new Date(),
|
|
113
126
|
updatedAt: new Date(),
|
|
114
127
|
value: domainVerificationToken,
|
|
@@ -199,15 +212,25 @@ export const verifyDomain = (options: SSOOptions) => {
|
|
|
199
212
|
});
|
|
200
213
|
}
|
|
201
214
|
|
|
215
|
+
const identifier = getVerificationIdentifier(
|
|
216
|
+
options,
|
|
217
|
+
provider.providerId,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (identifier.length > DNS_LABEL_MAX_LENGTH) {
|
|
221
|
+
throw new APIError("BAD_REQUEST", {
|
|
222
|
+
message: `Verification identifier exceeds the DNS label limit of ${DNS_LABEL_MAX_LENGTH} characters`,
|
|
223
|
+
code: "IDENTIFIER_TOO_LONG",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
202
227
|
const activeVerification =
|
|
203
228
|
await ctx.context.adapter.findOne<Verification>({
|
|
204
229
|
model: "verification",
|
|
205
230
|
where: [
|
|
206
231
|
{
|
|
207
232
|
field: "identifier",
|
|
208
|
-
value:
|
|
209
|
-
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
210
|
-
: `better-auth-token-${provider.providerId}`,
|
|
233
|
+
value: identifier,
|
|
211
234
|
},
|
|
212
235
|
{ field: "expiresAt", value: new Date(), operator: "gt" },
|
|
213
236
|
],
|
|
@@ -237,9 +260,8 @@ export const verifyDomain = (options: SSOOptions) => {
|
|
|
237
260
|
}
|
|
238
261
|
|
|
239
262
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
);
|
|
263
|
+
const hostname = new URL(provider.domain).hostname;
|
|
264
|
+
const dnsRecords = await dns.resolveTxt(`${identifier}.${hostname}`);
|
|
243
265
|
records = dnsRecords.flat();
|
|
244
266
|
} catch (error) {
|
|
245
267
|
ctx.context.logger.warn(
|