@better-auth/sso 1.4.0-beta.8 → 1.4.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/.turbo/turbo-build.log +14 -15
- package/dist/client.d.mts +18 -9
- package/dist/client.mjs +8 -6
- package/dist/index-D-JmJR9N.d.mts +853 -0
- package/dist/index.d.mts +2 -959
- package/dist/index.mjs +1481 -1649
- package/package.json +26 -19
- package/src/client.ts +20 -3
- package/src/domain-verification.test.ts +550 -0
- package/src/index.ts +83 -2210
- package/src/oidc.test.ts +8 -5
- package/src/routes/domain-verification.ts +275 -0
- package/src/routes/sso.ts +2182 -0
- package/src/saml.test.ts +226 -20
- package/src/types.ts +256 -0
- package/src/utils.ts +10 -0
- package/tsconfig.json +6 -4
- package/tsdown.config.ts +8 -0
- package/vitest.config.ts +3 -0
- package/build.config.ts +0 -12
- package/dist/client.cjs +0 -10
- package/dist/client.d.cts +0 -11
- package/dist/client.d.ts +0 -11
- package/dist/index.cjs +0 -1672
- package/dist/index.d.cts +0 -959
- package/dist/index.d.ts +0 -959
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.4.0
|
|
5
|
-
"
|
|
4
|
+
"version": "1.4.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.mjs",
|
|
7
|
+
"homepage": "https://www.better-auth.com/docs/plugins/sso",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/better-auth/better-auth",
|
|
11
|
+
"directory": "packages/sso"
|
|
12
|
+
},
|
|
6
13
|
"license": "MIT",
|
|
7
14
|
"keywords": [
|
|
8
15
|
"sso",
|
|
@@ -23,23 +30,23 @@
|
|
|
23
30
|
"description": "SSO plugin for Better Auth",
|
|
24
31
|
"exports": {
|
|
25
32
|
".": {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
33
|
+
"dev-source": "./src/index.ts",
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"default": "./dist/index.mjs"
|
|
29
36
|
},
|
|
30
37
|
"./client": {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
38
|
+
"dev-source": "./src/client.ts",
|
|
39
|
+
"types": "./dist/client.d.mts",
|
|
40
|
+
"default": "./dist/client.mjs"
|
|
34
41
|
}
|
|
35
42
|
},
|
|
36
43
|
"typesVersions": {
|
|
37
44
|
"*": {
|
|
38
45
|
"*": [
|
|
39
|
-
"./dist/index.d.
|
|
46
|
+
"./dist/index.d.mts"
|
|
40
47
|
],
|
|
41
48
|
"client": [
|
|
42
|
-
"./dist/client.d.
|
|
49
|
+
"./dist/client.d.mts"
|
|
43
50
|
]
|
|
44
51
|
}
|
|
45
52
|
},
|
|
@@ -47,26 +54,26 @@
|
|
|
47
54
|
"@better-fetch/fetch": "1.1.18",
|
|
48
55
|
"fast-xml-parser": "^5.2.5",
|
|
49
56
|
"jose": "^6.1.0",
|
|
50
|
-
"oauth2-mock-server": "^7.2.1",
|
|
51
57
|
"samlify": "^2.10.1",
|
|
52
|
-
"zod": "^4.1.
|
|
58
|
+
"zod": "^4.1.12"
|
|
53
59
|
},
|
|
54
60
|
"devDependencies": {
|
|
55
61
|
"@types/body-parser": "^1.19.6",
|
|
56
|
-
"@types/express": "^5.0.
|
|
57
|
-
"better-call": "1.0
|
|
62
|
+
"@types/express": "^5.0.5",
|
|
63
|
+
"better-call": "1.1.0",
|
|
58
64
|
"body-parser": "^2.2.0",
|
|
59
65
|
"express": "^5.1.0",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
66
|
+
"oauth2-mock-server": "^7.2.1",
|
|
67
|
+
"tsdown": "^0.16.0",
|
|
68
|
+
"better-auth": "1.4.0"
|
|
62
69
|
},
|
|
63
70
|
"peerDependencies": {
|
|
64
|
-
"better-auth": "1.4.0
|
|
71
|
+
"better-auth": "1.4.0"
|
|
65
72
|
},
|
|
66
73
|
"scripts": {
|
|
67
74
|
"test": "vitest",
|
|
68
|
-
"build": "
|
|
69
|
-
"dev": "
|
|
75
|
+
"build": "tsdown",
|
|
76
|
+
"dev": "tsdown --watch",
|
|
70
77
|
"typecheck": "tsc --project tsconfig.json"
|
|
71
78
|
}
|
|
72
79
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import type { BetterAuthClientPlugin } from "better-auth";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import type { SSOPlugin } from "./index";
|
|
3
|
+
|
|
4
|
+
interface SSOClientOptions {
|
|
5
|
+
domainVerification?:
|
|
6
|
+
| {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
| undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ssoClient = <CO extends SSOClientOptions>(
|
|
13
|
+
options?: CO | undefined,
|
|
14
|
+
) => {
|
|
4
15
|
return {
|
|
5
16
|
id: "sso-client",
|
|
6
|
-
$InferServerPlugin: {} as
|
|
17
|
+
$InferServerPlugin: {} as SSOPlugin<{
|
|
18
|
+
domainVerification: {
|
|
19
|
+
enabled: CO["domainVerification"] extends { enabled: true }
|
|
20
|
+
? true
|
|
21
|
+
: false;
|
|
22
|
+
};
|
|
23
|
+
}>,
|
|
7
24
|
} satisfies BetterAuthClientPlugin;
|
|
8
25
|
};
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
3
|
+
import { createAuthClient } from "better-auth/client";
|
|
4
|
+
import { setCookieToHeader } from "better-auth/cookies";
|
|
5
|
+
import { bearer, organization } from "better-auth/plugins";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { sso } from ".";
|
|
8
|
+
import { ssoClient } from "./client";
|
|
9
|
+
import type { SSOOptions } from "./types";
|
|
10
|
+
|
|
11
|
+
const dnsMock = vi.hoisted(() => {
|
|
12
|
+
return {
|
|
13
|
+
resolveTxt: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
vi.mock("node:dns/promises", () => {
|
|
18
|
+
return {
|
|
19
|
+
...dnsMock,
|
|
20
|
+
default: dnsMock,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("Domain verification", async () => {
|
|
25
|
+
type TestUser = { email: string; password: string; name: string };
|
|
26
|
+
const testUser: TestUser = {
|
|
27
|
+
email: "test@email.com",
|
|
28
|
+
password: "password",
|
|
29
|
+
name: "Test User",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createTestAuth = (options?: SSOOptions) => {
|
|
33
|
+
const data = {
|
|
34
|
+
user: [],
|
|
35
|
+
session: [],
|
|
36
|
+
verification: [],
|
|
37
|
+
account: [],
|
|
38
|
+
ssoProvider: [],
|
|
39
|
+
member: [],
|
|
40
|
+
organization: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const memory = memoryAdapter(data);
|
|
44
|
+
|
|
45
|
+
const ssoOptions = {
|
|
46
|
+
...options,
|
|
47
|
+
domainVerification: {
|
|
48
|
+
...options?.domainVerification,
|
|
49
|
+
enabled: true,
|
|
50
|
+
},
|
|
51
|
+
} satisfies SSOOptions;
|
|
52
|
+
|
|
53
|
+
const auth = betterAuth({
|
|
54
|
+
database: memory,
|
|
55
|
+
baseURL: "http://localhost:3000",
|
|
56
|
+
emailAndPassword: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
},
|
|
59
|
+
plugins: [sso(ssoOptions), organization()],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const authClient = createAuthClient({
|
|
63
|
+
baseURL: "http://localhost:3000",
|
|
64
|
+
plugins: [bearer(), ssoClient({ domainVerification: { enabled: true } })],
|
|
65
|
+
fetchOptions: {
|
|
66
|
+
customFetchImpl: async (url, init) => {
|
|
67
|
+
return auth.handler(new Request(url, init));
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
async function createOrganization(name: string, headers: Headers) {
|
|
73
|
+
return await auth.api.createOrganization({
|
|
74
|
+
body: {
|
|
75
|
+
name,
|
|
76
|
+
slug: name,
|
|
77
|
+
},
|
|
78
|
+
headers,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getAuthHeaders(user: TestUser, organizationId?: string) {
|
|
83
|
+
const headers = new Headers();
|
|
84
|
+
const response = await authClient.signUp.email({
|
|
85
|
+
email: user.email,
|
|
86
|
+
password: user.password,
|
|
87
|
+
name: user.name,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (response.data && organizationId) {
|
|
91
|
+
await auth.api.addMember({
|
|
92
|
+
body: {
|
|
93
|
+
userId: response.data.user.id,
|
|
94
|
+
role: "member",
|
|
95
|
+
},
|
|
96
|
+
headers,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await authClient.signIn.email(user, {
|
|
101
|
+
throw: true,
|
|
102
|
+
onSuccess: setCookieToHeader(headers),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return headers;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function registerSSOProvider(
|
|
109
|
+
headers: Headers,
|
|
110
|
+
organizationId?: string,
|
|
111
|
+
) {
|
|
112
|
+
return auth.api.registerSSOProvider({
|
|
113
|
+
body: {
|
|
114
|
+
providerId: "saml-provider-1",
|
|
115
|
+
issuer: "http://hello.com:8081",
|
|
116
|
+
domain: "http://hello.com:8081",
|
|
117
|
+
samlConfig: {
|
|
118
|
+
entryPoint: "http://idp.com:",
|
|
119
|
+
cert: "the-cert",
|
|
120
|
+
callbackUrl: "http://hello.com:8081/api/sso/saml2/callback",
|
|
121
|
+
spMetadata: {},
|
|
122
|
+
},
|
|
123
|
+
organizationId,
|
|
124
|
+
},
|
|
125
|
+
headers,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
auth,
|
|
131
|
+
authClient,
|
|
132
|
+
registerSSOProvider,
|
|
133
|
+
getAuthHeaders,
|
|
134
|
+
createOrganization,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
vi.clearAllMocks();
|
|
140
|
+
vi.useRealTimers();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("POST /sso/request-domain-verification", () => {
|
|
144
|
+
it("should return unauthorized when session is missing", async () => {
|
|
145
|
+
const { auth } = createTestAuth();
|
|
146
|
+
const response = await auth.api.requestDomainVerification({
|
|
147
|
+
body: {
|
|
148
|
+
providerId: "the-provider",
|
|
149
|
+
},
|
|
150
|
+
asResponse: true,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(response.status).toBe(401);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should return not found when no provider is found", async () => {
|
|
157
|
+
const { auth, getAuthHeaders } = createTestAuth();
|
|
158
|
+
const headers = await getAuthHeaders(testUser);
|
|
159
|
+
const response = await auth.api.requestDomainVerification({
|
|
160
|
+
body: {
|
|
161
|
+
providerId: "unknown",
|
|
162
|
+
},
|
|
163
|
+
headers,
|
|
164
|
+
asResponse: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(response.status).toBe(404);
|
|
168
|
+
expect(await response.json()).toEqual({
|
|
169
|
+
message: "Provider not found",
|
|
170
|
+
code: "PROVIDER_NOT_FOUND",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should return the existing active verification token", async () => {
|
|
175
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
176
|
+
const headers = await getAuthHeaders(testUser);
|
|
177
|
+
const provider = await registerSSOProvider(headers);
|
|
178
|
+
|
|
179
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
180
|
+
|
|
181
|
+
const newAuthHeaders = await getAuthHeaders(testUser);
|
|
182
|
+
|
|
183
|
+
const response = await auth.api.requestDomainVerification({
|
|
184
|
+
body: {
|
|
185
|
+
providerId: provider.providerId,
|
|
186
|
+
},
|
|
187
|
+
headers: newAuthHeaders,
|
|
188
|
+
asResponse: true,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(response.status).toBe(201);
|
|
192
|
+
expect(await response.json()).toEqual({
|
|
193
|
+
domainVerificationToken: provider.domainVerificationToken,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should return forbidden if user does not own the provider", async () => {
|
|
198
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
199
|
+
const headers = await getAuthHeaders(testUser);
|
|
200
|
+
const provider = await registerSSOProvider(headers);
|
|
201
|
+
|
|
202
|
+
const notOwnerHeaders = await getAuthHeaders({
|
|
203
|
+
name: "other",
|
|
204
|
+
email: "other@test.com",
|
|
205
|
+
password: "password",
|
|
206
|
+
});
|
|
207
|
+
const response = await auth.api.requestDomainVerification({
|
|
208
|
+
body: {
|
|
209
|
+
providerId: provider.providerId,
|
|
210
|
+
},
|
|
211
|
+
headers: notOwnerHeaders,
|
|
212
|
+
asResponse: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(response.status).toBe(403);
|
|
216
|
+
expect(await response.json()).toEqual({
|
|
217
|
+
message:
|
|
218
|
+
"User must be owner of or belong to the SSO provider organization",
|
|
219
|
+
code: "INSUFICCIENT_ACCESS",
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should return forbidden if user does not belong to the provider organization", async () => {
|
|
224
|
+
const { auth, getAuthHeaders, registerSSOProvider, createOrganization } =
|
|
225
|
+
createTestAuth();
|
|
226
|
+
const headers = await getAuthHeaders(testUser);
|
|
227
|
+
|
|
228
|
+
const orgA = await createOrganization("org-a", headers);
|
|
229
|
+
const orgB = await createOrganization("org-b", headers);
|
|
230
|
+
|
|
231
|
+
const provider = await registerSSOProvider(headers, orgA?.id);
|
|
232
|
+
|
|
233
|
+
const notOrgHeaders = await getAuthHeaders(
|
|
234
|
+
{
|
|
235
|
+
name: "other",
|
|
236
|
+
email: "other@test.com",
|
|
237
|
+
password: "password",
|
|
238
|
+
},
|
|
239
|
+
orgB?.id,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const response = await auth.api.requestDomainVerification({
|
|
243
|
+
body: {
|
|
244
|
+
providerId: provider.providerId,
|
|
245
|
+
},
|
|
246
|
+
headers: notOrgHeaders,
|
|
247
|
+
asResponse: true,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(response.status).toBe(403);
|
|
251
|
+
expect(await response.json()).toEqual({
|
|
252
|
+
message:
|
|
253
|
+
"User must be owner of or belong to the SSO provider organization",
|
|
254
|
+
code: "INSUFICCIENT_ACCESS",
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should return a new domain verification token", async () => {
|
|
259
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
260
|
+
const headers = await getAuthHeaders(testUser);
|
|
261
|
+
const provider = await registerSSOProvider(headers);
|
|
262
|
+
|
|
263
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
264
|
+
vi.advanceTimersByTime(Date.now() + 3600 * 24 * 7 * 1000 + 10); // advance 1 week + 10 seconds
|
|
265
|
+
|
|
266
|
+
const newHeaders = await getAuthHeaders(testUser);
|
|
267
|
+
const response = await auth.api.requestDomainVerification({
|
|
268
|
+
body: {
|
|
269
|
+
providerId: provider.providerId,
|
|
270
|
+
},
|
|
271
|
+
headers: newHeaders,
|
|
272
|
+
asResponse: true,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(response.status).toBe(201);
|
|
276
|
+
expect(await response.json()).toMatchObject({
|
|
277
|
+
domainVerificationToken: expect.any(String),
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should fail to create a new token on an already verified domain", async () => {
|
|
282
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
283
|
+
const headers = await getAuthHeaders(testUser);
|
|
284
|
+
const provider = await registerSSOProvider(headers);
|
|
285
|
+
|
|
286
|
+
dnsMock.resolveTxt.mockResolvedValue([
|
|
287
|
+
[
|
|
288
|
+
`better-auth-token-saml-provider-1=${provider.domainVerificationToken}`,
|
|
289
|
+
],
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
const domainVerificationResponse = await auth.api.verifyDomain({
|
|
293
|
+
body: {
|
|
294
|
+
providerId: provider.providerId,
|
|
295
|
+
},
|
|
296
|
+
headers,
|
|
297
|
+
asResponse: true,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(domainVerificationResponse.status).toBe(204);
|
|
301
|
+
|
|
302
|
+
const domainVerificationSubmissionResponse =
|
|
303
|
+
await auth.api.requestDomainVerification({
|
|
304
|
+
body: {
|
|
305
|
+
providerId: provider.providerId,
|
|
306
|
+
},
|
|
307
|
+
headers,
|
|
308
|
+
asResponse: true,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(domainVerificationSubmissionResponse.status).toBe(409);
|
|
312
|
+
expect(await domainVerificationSubmissionResponse.json()).toEqual({
|
|
313
|
+
message: "Domain has already been verified",
|
|
314
|
+
code: "DOMAIN_VERIFIED",
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("POST /sso/verify-domain", () => {
|
|
320
|
+
it("should return unauthorized when session is missing", async () => {
|
|
321
|
+
const { auth } = createTestAuth();
|
|
322
|
+
const response = await auth.api.verifyDomain({
|
|
323
|
+
body: {
|
|
324
|
+
providerId: "the-provider",
|
|
325
|
+
},
|
|
326
|
+
asResponse: true,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(response.status).toBe(401);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should return not found when no provider is found", async () => {
|
|
333
|
+
const { auth, getAuthHeaders } = createTestAuth();
|
|
334
|
+
const headers = await getAuthHeaders(testUser);
|
|
335
|
+
const response = await auth.api.verifyDomain({
|
|
336
|
+
body: {
|
|
337
|
+
providerId: "unknown",
|
|
338
|
+
},
|
|
339
|
+
headers,
|
|
340
|
+
asResponse: true,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(response.status).toBe(404);
|
|
344
|
+
expect(await response.json()).toEqual({
|
|
345
|
+
message: "Provider not found",
|
|
346
|
+
code: "PROVIDER_NOT_FOUND",
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should return not found when no pending verification is found", async () => {
|
|
351
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
352
|
+
const headers = await getAuthHeaders(testUser);
|
|
353
|
+
const provider = await registerSSOProvider(headers);
|
|
354
|
+
|
|
355
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
356
|
+
vi.advanceTimersByTime(Date.now() + 3600 * 24 * 7 * 1000 + 10); // advance 1 week + 10 seconds
|
|
357
|
+
|
|
358
|
+
const newAuthHeaders = await getAuthHeaders(testUser);
|
|
359
|
+
|
|
360
|
+
const response = await auth.api.verifyDomain({
|
|
361
|
+
body: {
|
|
362
|
+
providerId: provider.providerId,
|
|
363
|
+
},
|
|
364
|
+
headers: newAuthHeaders,
|
|
365
|
+
asResponse: true,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(response.status).toBe(404);
|
|
369
|
+
expect(await response.json()).toEqual({
|
|
370
|
+
message: "No pending domain verification exists",
|
|
371
|
+
code: "NO_PENDING_VERIFICATION",
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should return bad gateway when unable to verify domain", async () => {
|
|
376
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
377
|
+
const headers = await getAuthHeaders(testUser);
|
|
378
|
+
const provider = await registerSSOProvider(headers);
|
|
379
|
+
|
|
380
|
+
dnsMock.resolveTxt.mockResolvedValue([
|
|
381
|
+
["google-site-verification=the-token"],
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
const response = await auth.api.verifyDomain({
|
|
385
|
+
body: {
|
|
386
|
+
providerId: provider.providerId,
|
|
387
|
+
},
|
|
388
|
+
headers,
|
|
389
|
+
asResponse: true,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(response.status).toBe(502);
|
|
393
|
+
expect(await response.json()).toEqual({
|
|
394
|
+
message: "Unable to verify domain ownership. Try again later",
|
|
395
|
+
code: "DOMAIN_VERIFICATION_FAILED",
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("should return forbidden if user does not own the provider", async () => {
|
|
400
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
401
|
+
const headers = await getAuthHeaders(testUser);
|
|
402
|
+
const provider = await registerSSOProvider(headers);
|
|
403
|
+
|
|
404
|
+
const notOwnerHeaders = await getAuthHeaders({
|
|
405
|
+
name: "other",
|
|
406
|
+
email: "other@test.com",
|
|
407
|
+
password: "password",
|
|
408
|
+
});
|
|
409
|
+
const response = await auth.api.verifyDomain({
|
|
410
|
+
body: {
|
|
411
|
+
providerId: provider.providerId,
|
|
412
|
+
},
|
|
413
|
+
headers: notOwnerHeaders,
|
|
414
|
+
asResponse: true,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(response.status).toBe(403);
|
|
418
|
+
expect(await response.json()).toEqual({
|
|
419
|
+
message:
|
|
420
|
+
"User must be owner of or belong to the SSO provider organization",
|
|
421
|
+
code: "INSUFICCIENT_ACCESS",
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should return forbidden if user does not belong to the provider organization", async () => {
|
|
426
|
+
const { auth, getAuthHeaders, registerSSOProvider, createOrganization } =
|
|
427
|
+
createTestAuth();
|
|
428
|
+
const headers = await getAuthHeaders(testUser);
|
|
429
|
+
const orgA = await createOrganization("org-a", headers);
|
|
430
|
+
const orgB = await createOrganization("org-b", headers);
|
|
431
|
+
|
|
432
|
+
const provider = await registerSSOProvider(headers, orgA?.id);
|
|
433
|
+
|
|
434
|
+
const notOrgHeaders = await getAuthHeaders(
|
|
435
|
+
{
|
|
436
|
+
name: "other",
|
|
437
|
+
email: "other@test.com",
|
|
438
|
+
password: "password",
|
|
439
|
+
},
|
|
440
|
+
orgB?.id,
|
|
441
|
+
);
|
|
442
|
+
const response = await auth.api.verifyDomain({
|
|
443
|
+
body: {
|
|
444
|
+
providerId: provider.providerId,
|
|
445
|
+
},
|
|
446
|
+
headers: notOrgHeaders,
|
|
447
|
+
asResponse: true,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(response.status).toBe(403);
|
|
451
|
+
expect(await response.json()).toEqual({
|
|
452
|
+
message:
|
|
453
|
+
"User must be owner of or belong to the SSO provider organization",
|
|
454
|
+
code: "INSUFICCIENT_ACCESS",
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("should verify a provider domain ownership", async () => {
|
|
459
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
460
|
+
const headers = await getAuthHeaders(testUser);
|
|
461
|
+
const provider = await registerSSOProvider(headers);
|
|
462
|
+
|
|
463
|
+
expect(provider.domain).toBe("http://hello.com:8081");
|
|
464
|
+
expect(provider.domainVerified).toBe(false);
|
|
465
|
+
expect(provider.domainVerificationToken).toBeTypeOf("string");
|
|
466
|
+
|
|
467
|
+
dnsMock.resolveTxt.mockResolvedValue([
|
|
468
|
+
["google-site-verification=the-token"],
|
|
469
|
+
[
|
|
470
|
+
"v=spf1 ip4:50.242.118.232/29 include:_spf.google.com include:mail.zendesk.com ~all",
|
|
471
|
+
],
|
|
472
|
+
[
|
|
473
|
+
`better-auth-token-saml-provider-1=${provider.domainVerificationToken}`,
|
|
474
|
+
],
|
|
475
|
+
]);
|
|
476
|
+
|
|
477
|
+
const response = await auth.api.verifyDomain({
|
|
478
|
+
body: {
|
|
479
|
+
providerId: provider.providerId,
|
|
480
|
+
},
|
|
481
|
+
headers,
|
|
482
|
+
asResponse: true,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(response.status).toBe(204);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should verify a provider domain ownership (custom token verification prefix)", async () => {
|
|
489
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth({
|
|
490
|
+
domainVerification: { tokenPrefix: "auth-prefix" },
|
|
491
|
+
});
|
|
492
|
+
const headers = await getAuthHeaders(testUser);
|
|
493
|
+
const provider = await registerSSOProvider(headers);
|
|
494
|
+
|
|
495
|
+
dnsMock.resolveTxt.mockResolvedValue([
|
|
496
|
+
["google-site-verification=the-token"],
|
|
497
|
+
[
|
|
498
|
+
"v=spf1 ip4:50.242.118.232/29 include:_spf.google.com include:mail.zendesk.com ~all",
|
|
499
|
+
],
|
|
500
|
+
[`auth-prefix-saml-provider-1=${provider.domainVerificationToken}`],
|
|
501
|
+
]);
|
|
502
|
+
|
|
503
|
+
const response = await auth.api.verifyDomain({
|
|
504
|
+
body: {
|
|
505
|
+
providerId: provider.providerId,
|
|
506
|
+
},
|
|
507
|
+
headers,
|
|
508
|
+
asResponse: true,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
expect(response.status).toBe(204);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("should fail to verify an already verified domain", async () => {
|
|
515
|
+
const { auth, getAuthHeaders, registerSSOProvider } = createTestAuth();
|
|
516
|
+
const headers = await getAuthHeaders(testUser);
|
|
517
|
+
const provider = await registerSSOProvider(headers);
|
|
518
|
+
|
|
519
|
+
dnsMock.resolveTxt.mockResolvedValue([
|
|
520
|
+
[
|
|
521
|
+
`better-auth-token-saml-provider-1=${provider.domainVerificationToken}`,
|
|
522
|
+
],
|
|
523
|
+
]);
|
|
524
|
+
|
|
525
|
+
const firstResponse = await auth.api.verifyDomain({
|
|
526
|
+
body: {
|
|
527
|
+
providerId: provider.providerId,
|
|
528
|
+
},
|
|
529
|
+
headers,
|
|
530
|
+
asResponse: true,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(firstResponse.status).toBe(204);
|
|
534
|
+
|
|
535
|
+
const secondResponse = await auth.api.verifyDomain({
|
|
536
|
+
body: {
|
|
537
|
+
providerId: provider.providerId,
|
|
538
|
+
},
|
|
539
|
+
headers,
|
|
540
|
+
asResponse: true,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
expect(secondResponse.status).toBe(409);
|
|
544
|
+
expect(await secondResponse.json()).toEqual({
|
|
545
|
+
message: "Domain has already been verified",
|
|
546
|
+
code: "DOMAIN_VERIFIED",
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
});
|