@hammadj/better-auth-sso 1.5.0-beta.9
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 +116 -0
- package/LICENSE.md +20 -0
- package/dist/client.d.mts +10 -0
- package/dist/client.mjs +15 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +738 -0
- package/dist/index.mjs +2953 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
- package/src/client.ts +29 -0
- package/src/constants.ts +58 -0
- package/src/domain-verification.test.ts +551 -0
- package/src/index.ts +265 -0
- package/src/linking/index.ts +2 -0
- package/src/linking/org-assignment.test.ts +325 -0
- package/src/linking/org-assignment.ts +176 -0
- package/src/linking/types.ts +10 -0
- package/src/oidc/discovery.test.ts +1157 -0
- package/src/oidc/discovery.ts +494 -0
- package/src/oidc/errors.ts +92 -0
- package/src/oidc/index.ts +31 -0
- package/src/oidc/types.ts +219 -0
- package/src/oidc.test.ts +688 -0
- package/src/providers.test.ts +1326 -0
- package/src/routes/domain-verification.ts +275 -0
- package/src/routes/providers.ts +565 -0
- package/src/routes/schemas.ts +96 -0
- package/src/routes/sso.ts +2750 -0
- package/src/saml/algorithms.test.ts +449 -0
- package/src/saml/algorithms.ts +338 -0
- package/src/saml/assertions.test.ts +239 -0
- package/src/saml/assertions.ts +62 -0
- package/src/saml/index.ts +13 -0
- package/src/saml/parser.ts +56 -0
- package/src/saml-state.ts +78 -0
- package/src/saml.test.ts +4319 -0
- package/src/types.ts +365 -0
- package/src/utils.test.ts +103 -0
- package/src/utils.ts +81 -0
- package/tsconfig.json +14 -0
- package/tsdown.config.ts +9 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,1326 @@
|
|
|
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 { organization } from "better-auth/plugins";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { sso } from ".";
|
|
8
|
+
import { ssoClient } from "./client";
|
|
9
|
+
|
|
10
|
+
const TEST_CERT = `MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiUMA0Gcm9markup
|
|
11
|
+
temporary cert for testing`;
|
|
12
|
+
|
|
13
|
+
describe("SSO provider read endpoints", () => {
|
|
14
|
+
type TestUser = { email: string; password: string; name: string };
|
|
15
|
+
|
|
16
|
+
interface SSOProviderData {
|
|
17
|
+
id: string;
|
|
18
|
+
providerId: string;
|
|
19
|
+
issuer: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
organizationId?: string;
|
|
23
|
+
domainVerified?: boolean;
|
|
24
|
+
samlConfig?: string;
|
|
25
|
+
oidcConfig?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createTestAuth = (
|
|
29
|
+
includeOrgPlugin = true,
|
|
30
|
+
enableDomainVerification = false,
|
|
31
|
+
) => {
|
|
32
|
+
const data: {
|
|
33
|
+
user: { id: string; email: string }[];
|
|
34
|
+
session: object[];
|
|
35
|
+
verification: object[];
|
|
36
|
+
account: object[];
|
|
37
|
+
ssoProvider: SSOProviderData[];
|
|
38
|
+
member: object[];
|
|
39
|
+
organization: object[];
|
|
40
|
+
} = {
|
|
41
|
+
user: [],
|
|
42
|
+
session: [],
|
|
43
|
+
verification: [],
|
|
44
|
+
account: [],
|
|
45
|
+
ssoProvider: [],
|
|
46
|
+
member: [],
|
|
47
|
+
organization: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const memory = memoryAdapter(data);
|
|
51
|
+
|
|
52
|
+
const ssoPlugin = enableDomainVerification
|
|
53
|
+
? sso({ domainVerification: { enabled: true } })
|
|
54
|
+
: sso();
|
|
55
|
+
const plugins = includeOrgPlugin
|
|
56
|
+
? [ssoPlugin, organization()]
|
|
57
|
+
: [ssoPlugin];
|
|
58
|
+
|
|
59
|
+
const auth = betterAuth({
|
|
60
|
+
database: memory,
|
|
61
|
+
baseURL: "http://localhost:3000",
|
|
62
|
+
emailAndPassword: {
|
|
63
|
+
enabled: true,
|
|
64
|
+
},
|
|
65
|
+
plugins,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const authClient = createAuthClient({
|
|
69
|
+
baseURL: "http://localhost:3000",
|
|
70
|
+
plugins: [ssoClient()],
|
|
71
|
+
fetchOptions: {
|
|
72
|
+
customFetchImpl: async (url, init) => {
|
|
73
|
+
return auth.handler(new Request(url, init));
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
async function getAuthHeaders(user: TestUser) {
|
|
79
|
+
const headers = new Headers();
|
|
80
|
+
await authClient.signUp.email({
|
|
81
|
+
email: user.email,
|
|
82
|
+
password: user.password,
|
|
83
|
+
name: user.name,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await authClient.signIn.email(user, {
|
|
87
|
+
throw: true,
|
|
88
|
+
onSuccess: setCookieToHeader(headers),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return headers;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function createOrganization(name: string, headers: Headers) {
|
|
95
|
+
return auth.api.createOrganization({
|
|
96
|
+
body: {
|
|
97
|
+
name,
|
|
98
|
+
slug: name,
|
|
99
|
+
},
|
|
100
|
+
headers,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function addMember(
|
|
105
|
+
userId: string,
|
|
106
|
+
organizationId: string,
|
|
107
|
+
role: "member" | "admin" | "owner",
|
|
108
|
+
headers: Headers,
|
|
109
|
+
) {
|
|
110
|
+
return auth.api.addMember({
|
|
111
|
+
body: {
|
|
112
|
+
userId,
|
|
113
|
+
role,
|
|
114
|
+
organizationId,
|
|
115
|
+
},
|
|
116
|
+
headers,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function registerSAMLProvider(
|
|
121
|
+
headers: Headers,
|
|
122
|
+
providerId: string,
|
|
123
|
+
organizationId?: string,
|
|
124
|
+
) {
|
|
125
|
+
return auth.api.registerSSOProvider({
|
|
126
|
+
body: {
|
|
127
|
+
providerId,
|
|
128
|
+
issuer: "https://idp.example.com",
|
|
129
|
+
domain: "example.com",
|
|
130
|
+
samlConfig: {
|
|
131
|
+
entryPoint: "https://idp.example.com/sso",
|
|
132
|
+
cert: TEST_CERT,
|
|
133
|
+
callbackUrl: "http://localhost:3000/api/sso/callback",
|
|
134
|
+
audience: "my-audience",
|
|
135
|
+
wantAssertionsSigned: true,
|
|
136
|
+
spMetadata: {},
|
|
137
|
+
},
|
|
138
|
+
organizationId,
|
|
139
|
+
},
|
|
140
|
+
headers,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createOIDCProviderData(
|
|
145
|
+
userId: string,
|
|
146
|
+
providerId: string,
|
|
147
|
+
clientId: string,
|
|
148
|
+
organizationId?: string,
|
|
149
|
+
) {
|
|
150
|
+
data.ssoProvider.push({
|
|
151
|
+
id: `oidc-${providerId}`,
|
|
152
|
+
providerId,
|
|
153
|
+
issuer: "https://idp.example.com",
|
|
154
|
+
domain: "example.com",
|
|
155
|
+
userId,
|
|
156
|
+
organizationId,
|
|
157
|
+
oidcConfig: JSON.stringify({
|
|
158
|
+
clientId,
|
|
159
|
+
clientSecret: "super-secret-value",
|
|
160
|
+
discoveryEndpoint: "https://idp.example.com/.well-known",
|
|
161
|
+
pkce: true,
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
auth,
|
|
168
|
+
authClient,
|
|
169
|
+
data,
|
|
170
|
+
getAuthHeaders,
|
|
171
|
+
createOrganization,
|
|
172
|
+
addMember,
|
|
173
|
+
registerSAMLProvider,
|
|
174
|
+
createOIDCProviderData,
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
describe("GET /sso/providers", () => {
|
|
179
|
+
it("should return 401 when not authenticated", async () => {
|
|
180
|
+
const { auth } = createTestAuth();
|
|
181
|
+
const response = await auth.api.listSSOProviders({
|
|
182
|
+
asResponse: true,
|
|
183
|
+
});
|
|
184
|
+
expect(response.status).toBe(401);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should return empty list when no providers exist", async () => {
|
|
188
|
+
const { auth, getAuthHeaders } = createTestAuth();
|
|
189
|
+
const headers = await getAuthHeaders({
|
|
190
|
+
email: "test@example.com",
|
|
191
|
+
password: "password123",
|
|
192
|
+
name: "Test User",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const response = await auth.api.listSSOProviders({ headers });
|
|
196
|
+
expect(response.providers).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should return only providers owned by the user", async () => {
|
|
200
|
+
const { auth, getAuthHeaders, registerSAMLProvider, data } =
|
|
201
|
+
createTestAuth(false);
|
|
202
|
+
|
|
203
|
+
const ownerHeaders = await getAuthHeaders({
|
|
204
|
+
email: "owner@example.com",
|
|
205
|
+
password: "password123",
|
|
206
|
+
name: "Owner",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await registerSAMLProvider(ownerHeaders, "my-saml-provider");
|
|
210
|
+
|
|
211
|
+
// Create another user's provider by pushing directly to data
|
|
212
|
+
data.ssoProvider.push({
|
|
213
|
+
id: "provider-2",
|
|
214
|
+
providerId: "other-provider",
|
|
215
|
+
issuer: "https://other.com",
|
|
216
|
+
domain: "other.com",
|
|
217
|
+
userId: "different-user-id",
|
|
218
|
+
oidcConfig: JSON.stringify({
|
|
219
|
+
clientId: "client123456",
|
|
220
|
+
clientSecret: "secret",
|
|
221
|
+
discoveryEndpoint: "https://other.com/.well-known",
|
|
222
|
+
pkce: true,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const response = await auth.api.listSSOProviders({
|
|
227
|
+
headers: ownerHeaders,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(response.providers).toHaveLength(1);
|
|
231
|
+
expect(response.providers[0]!.providerId).toBe("my-saml-provider");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should return providers for org admin when org plugin enabled", async () => {
|
|
235
|
+
const { auth, getAuthHeaders, createOrganization, registerSAMLProvider } =
|
|
236
|
+
createTestAuth(true);
|
|
237
|
+
|
|
238
|
+
const adminHeaders = await getAuthHeaders({
|
|
239
|
+
email: "admin@example.com",
|
|
240
|
+
password: "password123",
|
|
241
|
+
name: "Admin",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const org = await createOrganization("test-org", adminHeaders);
|
|
245
|
+
|
|
246
|
+
await registerSAMLProvider(adminHeaders, "org-saml-provider", org!.id);
|
|
247
|
+
|
|
248
|
+
const response = await auth.api.listSSOProviders({
|
|
249
|
+
headers: adminHeaders,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(response.providers).toHaveLength(1);
|
|
253
|
+
expect(response.providers[0]!.providerId).toBe("org-saml-provider");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should return providers for org owner", async () => {
|
|
257
|
+
const { auth, getAuthHeaders, createOrganization, registerSAMLProvider } =
|
|
258
|
+
createTestAuth(true);
|
|
259
|
+
|
|
260
|
+
const ownerHeaders = await getAuthHeaders({
|
|
261
|
+
email: "owner@example.com",
|
|
262
|
+
password: "password123",
|
|
263
|
+
name: "Owner",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const org = await createOrganization("test-org", ownerHeaders);
|
|
267
|
+
|
|
268
|
+
await registerSAMLProvider(ownerHeaders, "org-saml-provider", org!.id);
|
|
269
|
+
|
|
270
|
+
const response = await auth.api.listSSOProviders({
|
|
271
|
+
headers: ownerHeaders,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(response.providers).toHaveLength(1);
|
|
275
|
+
expect(response.providers[0]!.providerId).toBe("org-saml-provider");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should handle comma-separated roles", async () => {
|
|
279
|
+
const {
|
|
280
|
+
auth,
|
|
281
|
+
getAuthHeaders,
|
|
282
|
+
createOrganization,
|
|
283
|
+
registerSAMLProvider,
|
|
284
|
+
data,
|
|
285
|
+
} = createTestAuth(true);
|
|
286
|
+
|
|
287
|
+
const creatorHeaders = await getAuthHeaders({
|
|
288
|
+
email: "creator@example.com",
|
|
289
|
+
password: "password123",
|
|
290
|
+
name: "Creator",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const org = await createOrganization("test-org", creatorHeaders);
|
|
294
|
+
|
|
295
|
+
await registerSAMLProvider(creatorHeaders, "org-saml-provider", org!.id);
|
|
296
|
+
|
|
297
|
+
const multiHeaders = await getAuthHeaders({
|
|
298
|
+
email: "multi@example.com",
|
|
299
|
+
password: "password123",
|
|
300
|
+
name: "Multi Role User",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const multiUser = data.user.find((u) => u.email === "multi@example.com");
|
|
304
|
+
|
|
305
|
+
// Push directly to test comma-separated roles (API doesn't accept this format)
|
|
306
|
+
data.member.push({
|
|
307
|
+
id: "multi-member",
|
|
308
|
+
userId: multiUser!.id,
|
|
309
|
+
organizationId: org!.id,
|
|
310
|
+
role: "admin,member",
|
|
311
|
+
createdAt: new Date(),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const response = await auth.api.listSSOProviders({
|
|
315
|
+
headers: multiHeaders,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(response.providers).toHaveLength(1);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should not return org providers to non-admin members", async () => {
|
|
322
|
+
const {
|
|
323
|
+
auth,
|
|
324
|
+
getAuthHeaders,
|
|
325
|
+
createOrganization,
|
|
326
|
+
addMember,
|
|
327
|
+
registerSAMLProvider,
|
|
328
|
+
data,
|
|
329
|
+
} = createTestAuth(true);
|
|
330
|
+
|
|
331
|
+
const creatorHeaders = await getAuthHeaders({
|
|
332
|
+
email: "creator@example.com",
|
|
333
|
+
password: "password123",
|
|
334
|
+
name: "Creator",
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const org = await createOrganization("test-org", creatorHeaders);
|
|
338
|
+
|
|
339
|
+
await registerSAMLProvider(creatorHeaders, "org-saml-provider", org!.id);
|
|
340
|
+
|
|
341
|
+
const memberHeaders = await getAuthHeaders({
|
|
342
|
+
email: "member@example.com",
|
|
343
|
+
password: "password123",
|
|
344
|
+
name: "Member",
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const memberUser = (data.user as { id: string; email: string }[]).find(
|
|
348
|
+
(u) => u.email === "member@example.com",
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
await addMember(memberUser!.id, org!.id, "member", creatorHeaders);
|
|
352
|
+
|
|
353
|
+
const response = await auth.api.listSSOProviders({
|
|
354
|
+
headers: memberHeaders,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(response.providers).toHaveLength(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should return provider with organizationId when org plugin is disabled if user owns it", async () => {
|
|
361
|
+
const { auth, getAuthHeaders, data } = createTestAuth(false);
|
|
362
|
+
|
|
363
|
+
const ownerHeaders = await getAuthHeaders({
|
|
364
|
+
email: "owner@example.com",
|
|
365
|
+
password: "password123",
|
|
366
|
+
name: "Owner",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const ownerUser = (data.user as { id: string; email: string }[]).find(
|
|
370
|
+
(u) => u.email === "owner@example.com",
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Create a provider with organizationId but org plugin is disabled
|
|
374
|
+
// User should still be able to access it if they own it
|
|
375
|
+
data.ssoProvider.push({
|
|
376
|
+
id: "provider-with-org-id",
|
|
377
|
+
providerId: "my-provider",
|
|
378
|
+
issuer: "https://idp.example.com",
|
|
379
|
+
domain: "example.com",
|
|
380
|
+
userId: ownerUser!.id,
|
|
381
|
+
organizationId: "some-org-id",
|
|
382
|
+
samlConfig: JSON.stringify({
|
|
383
|
+
entryPoint: "https://idp.example.com/sso",
|
|
384
|
+
cert: TEST_CERT,
|
|
385
|
+
callbackUrl: "http://localhost:3000/api/sso/callback",
|
|
386
|
+
audience: "my-audience",
|
|
387
|
+
wantAssertionsSigned: true,
|
|
388
|
+
spMetadata: {},
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const response = await auth.api.listSSOProviders({
|
|
393
|
+
headers: ownerHeaders,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(response.providers).toHaveLength(1);
|
|
397
|
+
expect(response.providers[0]!.providerId).toBe("my-provider");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should require org admin access for user-owned provider with organizationId when org plugin enabled", async () => {
|
|
401
|
+
const { auth, getAuthHeaders, createOrganization, data } =
|
|
402
|
+
createTestAuth(true);
|
|
403
|
+
|
|
404
|
+
const ownerHeaders = await getAuthHeaders({
|
|
405
|
+
email: "owner@example.com",
|
|
406
|
+
password: "password123",
|
|
407
|
+
name: "Owner",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const org = await createOrganization("test-org", ownerHeaders);
|
|
411
|
+
|
|
412
|
+
// Create a provider where the user owns it (userId matches) but it's in an org
|
|
413
|
+
// When org plugin is enabled, org admin access should be required, not just ownership
|
|
414
|
+
const ownerUser = (data.user as { id: string; email: string }[]).find(
|
|
415
|
+
(u) => u.email === "owner@example.com",
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
data.ssoProvider.push({
|
|
419
|
+
id: "provider-owned-by-user-in-org",
|
|
420
|
+
providerId: "user-owned-org-provider",
|
|
421
|
+
issuer: "https://idp.example.com",
|
|
422
|
+
domain: "example.com",
|
|
423
|
+
userId: ownerUser!.id,
|
|
424
|
+
organizationId: org!.id,
|
|
425
|
+
samlConfig: JSON.stringify({
|
|
426
|
+
entryPoint: "https://idp.example.com/sso",
|
|
427
|
+
cert: TEST_CERT,
|
|
428
|
+
callbackUrl: "http://localhost:3000/api/sso/callback",
|
|
429
|
+
audience: "my-audience",
|
|
430
|
+
wantAssertionsSigned: true,
|
|
431
|
+
spMetadata: {},
|
|
432
|
+
}),
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Owner should be able to access it since they created the org (are admin)
|
|
436
|
+
const ownerResponse = await auth.api.listSSOProviders({
|
|
437
|
+
headers: ownerHeaders,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(ownerResponse.providers).toHaveLength(1);
|
|
441
|
+
expect(ownerResponse.providers[0]!.providerId).toBe(
|
|
442
|
+
"user-owned-org-provider",
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// Create another user who is NOT an org admin
|
|
446
|
+
const nonAdminHeaders = await getAuthHeaders({
|
|
447
|
+
email: "nonadmin@example.com",
|
|
448
|
+
password: "password123",
|
|
449
|
+
name: "Non Admin",
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const nonAdminResponse = await auth.api.listSSOProviders({
|
|
453
|
+
headers: nonAdminHeaders,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Non-admin should not see it even though they might have the same userId logic elsewhere
|
|
457
|
+
// This tests that org admin check takes precedence when org plugin is enabled
|
|
458
|
+
expect(nonAdminResponse.providers).toHaveLength(0);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("GET /sso/providers/:providerId", () => {
|
|
463
|
+
it("should return 401 when not authenticated", async () => {
|
|
464
|
+
const { auth } = createTestAuth();
|
|
465
|
+
const response = await auth.api.getSSOProvider({
|
|
466
|
+
params: { providerId: "test" },
|
|
467
|
+
asResponse: true,
|
|
468
|
+
});
|
|
469
|
+
expect(response.status).toBe(401);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("should return 404 when provider not found", async () => {
|
|
473
|
+
const { auth, getAuthHeaders } = createTestAuth();
|
|
474
|
+
const headers = await getAuthHeaders({
|
|
475
|
+
email: "test@example.com",
|
|
476
|
+
password: "password123",
|
|
477
|
+
name: "Test User",
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const response = await auth.api.getSSOProvider({
|
|
481
|
+
params: { providerId: "nonexistent" },
|
|
482
|
+
headers,
|
|
483
|
+
asResponse: true,
|
|
484
|
+
});
|
|
485
|
+
expect(response.status).toBe(404);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should return 403 when user does not own provider", async () => {
|
|
489
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
490
|
+
createTestAuth(false);
|
|
491
|
+
|
|
492
|
+
const ownerHeaders = await getAuthHeaders({
|
|
493
|
+
email: "owner@example.com",
|
|
494
|
+
password: "password123",
|
|
495
|
+
name: "Owner",
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
await registerSAMLProvider(ownerHeaders, "other-provider");
|
|
499
|
+
|
|
500
|
+
const otherHeaders = await getAuthHeaders({
|
|
501
|
+
email: "other@example.com",
|
|
502
|
+
password: "password123",
|
|
503
|
+
name: "Other User",
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const response = await auth.api.getSSOProvider({
|
|
507
|
+
params: { providerId: "other-provider" },
|
|
508
|
+
headers: otherHeaders,
|
|
509
|
+
asResponse: true,
|
|
510
|
+
});
|
|
511
|
+
expect(response.status).toBe(403);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("should return sanitized SAML provider details", async () => {
|
|
515
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
516
|
+
createTestAuth(false);
|
|
517
|
+
|
|
518
|
+
const headers = await getAuthHeaders({
|
|
519
|
+
email: "owner@example.com",
|
|
520
|
+
password: "password123",
|
|
521
|
+
name: "Owner",
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
525
|
+
|
|
526
|
+
const response = await auth.api.getSSOProvider({
|
|
527
|
+
params: { providerId: "my-saml-provider" },
|
|
528
|
+
headers,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(response.providerId).toBe("my-saml-provider");
|
|
532
|
+
expect(response.type).toBe("saml");
|
|
533
|
+
expect(response.issuer).toBe("https://idp.example.com");
|
|
534
|
+
expect(response.samlConfig).toBeDefined();
|
|
535
|
+
expect(response.samlConfig?.entryPoint).toBe(
|
|
536
|
+
"https://idp.example.com/sso",
|
|
537
|
+
);
|
|
538
|
+
expect(response.samlConfig?.certificate).toBeDefined();
|
|
539
|
+
expect(response.spMetadataUrl).toContain("/sso/saml2/sp/metadata");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("should return sanitized OIDC provider with masked clientId", async () => {
|
|
543
|
+
const { auth, getAuthHeaders, createOIDCProviderData, data } =
|
|
544
|
+
createTestAuth(false);
|
|
545
|
+
|
|
546
|
+
const headers = await getAuthHeaders({
|
|
547
|
+
email: "owner@example.com",
|
|
548
|
+
password: "password123",
|
|
549
|
+
name: "Owner",
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
553
|
+
(u) => u.email === "owner@example.com",
|
|
554
|
+
);
|
|
555
|
+
createOIDCProviderData(
|
|
556
|
+
user!.id,
|
|
557
|
+
"my-oidc-provider",
|
|
558
|
+
"my-client-id-12345",
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const response = await auth.api.getSSOProvider({
|
|
562
|
+
params: { providerId: "my-oidc-provider" },
|
|
563
|
+
headers,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
expect(response.providerId).toBe("my-oidc-provider");
|
|
567
|
+
expect(response.type).toBe("oidc");
|
|
568
|
+
expect(response.oidcConfig).toBeDefined();
|
|
569
|
+
expect(response.oidcConfig?.clientIdLastFour).toBe("****2345");
|
|
570
|
+
expect(response.oidcConfig?.discoveryEndpoint).toBe(
|
|
571
|
+
"https://idp.example.com/.well-known",
|
|
572
|
+
);
|
|
573
|
+
expect(response.oidcConfig).not.toHaveProperty("clientSecret");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("should not leak clientSecret in response", async () => {
|
|
577
|
+
const { auth, getAuthHeaders, createOIDCProviderData, data } =
|
|
578
|
+
createTestAuth(false);
|
|
579
|
+
|
|
580
|
+
const headers = await getAuthHeaders({
|
|
581
|
+
email: "owner@example.com",
|
|
582
|
+
password: "password123",
|
|
583
|
+
name: "Owner",
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
587
|
+
(u) => u.email === "owner@example.com",
|
|
588
|
+
);
|
|
589
|
+
createOIDCProviderData(user!.id, "my-oidc-provider", "client123");
|
|
590
|
+
|
|
591
|
+
const response = await auth.api.getSSOProvider({
|
|
592
|
+
params: { providerId: "my-oidc-provider" },
|
|
593
|
+
headers,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const responseStr = JSON.stringify(response);
|
|
597
|
+
expect(responseStr).not.toContain("super-secret-value");
|
|
598
|
+
expect(responseStr).not.toContain("clientSecret");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("should allow access to provider with organizationId when org plugin is disabled if user owns it", async () => {
|
|
602
|
+
const { auth, getAuthHeaders, data } = createTestAuth(false);
|
|
603
|
+
|
|
604
|
+
const headers = await getAuthHeaders({
|
|
605
|
+
email: "owner@example.com",
|
|
606
|
+
password: "password123",
|
|
607
|
+
name: "Owner",
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
611
|
+
(u) => u.email === "owner@example.com",
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
data.ssoProvider.push({
|
|
615
|
+
id: "provider-with-org-id",
|
|
616
|
+
providerId: "my-provider",
|
|
617
|
+
issuer: "https://idp.example.com",
|
|
618
|
+
domain: "example.com",
|
|
619
|
+
userId: user!.id,
|
|
620
|
+
organizationId: "some-org-id",
|
|
621
|
+
samlConfig: JSON.stringify({
|
|
622
|
+
entryPoint: "https://idp.example.com/sso",
|
|
623
|
+
cert: TEST_CERT,
|
|
624
|
+
callbackUrl: "http://localhost:3000/api/sso/callback",
|
|
625
|
+
audience: "my-audience",
|
|
626
|
+
wantAssertionsSigned: true,
|
|
627
|
+
spMetadata: {},
|
|
628
|
+
}),
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const response = await auth.api.getSSOProvider({
|
|
632
|
+
params: { providerId: "my-provider" },
|
|
633
|
+
headers,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
expect(response.providerId).toBe("my-provider");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("should require org admin access for user-owned provider with organizationId when org plugin enabled", async () => {
|
|
640
|
+
const { auth, getAuthHeaders, createOrganization, data } =
|
|
641
|
+
createTestAuth(true);
|
|
642
|
+
|
|
643
|
+
const ownerHeaders = await getAuthHeaders({
|
|
644
|
+
email: "owner@example.com",
|
|
645
|
+
password: "password123",
|
|
646
|
+
name: "Owner",
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const org = await createOrganization("test-org", ownerHeaders);
|
|
650
|
+
|
|
651
|
+
const ownerUser = (data.user as { id: string; email: string }[]).find(
|
|
652
|
+
(u) => u.email === "owner@example.com",
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
data.ssoProvider.push({
|
|
656
|
+
id: "provider-owned-by-user-in-org",
|
|
657
|
+
providerId: "user-owned-org-provider",
|
|
658
|
+
issuer: "https://idp.example.com",
|
|
659
|
+
domain: "example.com",
|
|
660
|
+
userId: ownerUser!.id,
|
|
661
|
+
organizationId: org!.id,
|
|
662
|
+
samlConfig: JSON.stringify({
|
|
663
|
+
entryPoint: "https://idp.example.com/sso",
|
|
664
|
+
cert: TEST_CERT,
|
|
665
|
+
callbackUrl: "http://localhost:3000/api/sso/callback",
|
|
666
|
+
audience: "my-audience",
|
|
667
|
+
wantAssertionsSigned: true,
|
|
668
|
+
spMetadata: {},
|
|
669
|
+
}),
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Owner should be able to access it since they created the org (are admin)
|
|
673
|
+
const ownerResponse = await auth.api.getSSOProvider({
|
|
674
|
+
params: { providerId: "user-owned-org-provider" },
|
|
675
|
+
headers: ownerHeaders,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
expect(ownerResponse.providerId).toBe("user-owned-org-provider");
|
|
679
|
+
|
|
680
|
+
// Create another user who is NOT an org admin
|
|
681
|
+
const nonAdminHeaders = await getAuthHeaders({
|
|
682
|
+
email: "nonadmin@example.com",
|
|
683
|
+
password: "password123",
|
|
684
|
+
name: "Non Admin",
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const nonAdminResponse = await auth.api.getSSOProvider({
|
|
688
|
+
params: { providerId: "user-owned-org-provider" },
|
|
689
|
+
headers: nonAdminHeaders,
|
|
690
|
+
asResponse: true,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Non-admin should get 403 even though they might own providers elsewhere
|
|
694
|
+
expect(nonAdminResponse.status).toBe(403);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe("sanitization", () => {
|
|
699
|
+
it("should not expose raw certificate PEM", async () => {
|
|
700
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
701
|
+
createTestAuth(false);
|
|
702
|
+
|
|
703
|
+
const headers = await getAuthHeaders({
|
|
704
|
+
email: "owner@example.com",
|
|
705
|
+
password: "password123",
|
|
706
|
+
name: "Owner",
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
710
|
+
|
|
711
|
+
const response = await auth.api.getSSOProvider({
|
|
712
|
+
params: { providerId: "my-saml-provider" },
|
|
713
|
+
headers,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const responseStr = JSON.stringify(response);
|
|
717
|
+
expect(responseStr).not.toContain("BEGIN CERTIFICATE");
|
|
718
|
+
expect(responseStr).not.toContain(TEST_CERT);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("should handle certificate parse errors gracefully", async () => {
|
|
722
|
+
const { auth, getAuthHeaders, data } = createTestAuth(false);
|
|
723
|
+
|
|
724
|
+
const headers = await getAuthHeaders({
|
|
725
|
+
email: "owner@example.com",
|
|
726
|
+
password: "password123",
|
|
727
|
+
name: "Owner",
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
731
|
+
(u) => u.email === "owner@example.com",
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
data.ssoProvider.push({
|
|
735
|
+
id: "provider-1",
|
|
736
|
+
providerId: "my-saml-provider",
|
|
737
|
+
issuer: "https://idp.example.com",
|
|
738
|
+
domain: "example.com",
|
|
739
|
+
userId: user!.id,
|
|
740
|
+
samlConfig: JSON.stringify({
|
|
741
|
+
entryPoint: "https://idp.example.com/sso",
|
|
742
|
+
cert: "invalid-cert-data",
|
|
743
|
+
callbackUrl: "http://localhost:3000/api/sso/callback",
|
|
744
|
+
}),
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const response = await auth.api.getSSOProvider({
|
|
748
|
+
params: { providerId: "my-saml-provider" },
|
|
749
|
+
headers,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
expect(response.samlConfig?.certificate).toBeDefined();
|
|
753
|
+
expect(
|
|
754
|
+
(response.samlConfig?.certificate as { error?: string })?.error,
|
|
755
|
+
).toBe("Failed to parse certificate");
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it("should mask short clientId with just asterisks", async () => {
|
|
759
|
+
const { auth, getAuthHeaders, createOIDCProviderData, data } =
|
|
760
|
+
createTestAuth(false);
|
|
761
|
+
|
|
762
|
+
const headers = await getAuthHeaders({
|
|
763
|
+
email: "owner@example.com",
|
|
764
|
+
password: "password123",
|
|
765
|
+
name: "Owner",
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
769
|
+
(u) => u.email === "owner@example.com",
|
|
770
|
+
);
|
|
771
|
+
createOIDCProviderData(user!.id, "my-oidc-provider", "abc");
|
|
772
|
+
|
|
773
|
+
const response = await auth.api.getSSOProvider({
|
|
774
|
+
params: { providerId: "my-oidc-provider" },
|
|
775
|
+
headers,
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
expect(response.oidcConfig?.clientIdLastFour).toBe("****");
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
describe("PATCH /sso/providers/:providerId", () => {
|
|
783
|
+
it("should return 401 when not authenticated", async () => {
|
|
784
|
+
const { auth } = createTestAuth();
|
|
785
|
+
const response = await auth.api.updateSSOProvider({
|
|
786
|
+
params: { providerId: "test" },
|
|
787
|
+
body: { domain: "new-domain.com" },
|
|
788
|
+
asResponse: true,
|
|
789
|
+
});
|
|
790
|
+
expect(response.status).toBe(401);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("should return 404 when provider not found", async () => {
|
|
794
|
+
const { auth, getAuthHeaders } = createTestAuth();
|
|
795
|
+
const headers = await getAuthHeaders({
|
|
796
|
+
email: "test@example.com",
|
|
797
|
+
password: "password123",
|
|
798
|
+
name: "Test User",
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const response = await auth.api.updateSSOProvider({
|
|
802
|
+
params: { providerId: "nonexistent" },
|
|
803
|
+
body: { domain: "new-domain.com" },
|
|
804
|
+
headers,
|
|
805
|
+
asResponse: true,
|
|
806
|
+
});
|
|
807
|
+
expect(response.status).toBe(404);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("should return 403 when user does not own provider", async () => {
|
|
811
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
812
|
+
createTestAuth(false);
|
|
813
|
+
|
|
814
|
+
const ownerHeaders = await getAuthHeaders({
|
|
815
|
+
email: "owner@example.com",
|
|
816
|
+
password: "password123",
|
|
817
|
+
name: "Owner",
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
await registerSAMLProvider(ownerHeaders, "other-provider");
|
|
821
|
+
|
|
822
|
+
const otherHeaders = await getAuthHeaders({
|
|
823
|
+
email: "other@example.com",
|
|
824
|
+
password: "password123",
|
|
825
|
+
name: "Other User",
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const response = await auth.api.updateSSOProvider({
|
|
829
|
+
params: { providerId: "other-provider" },
|
|
830
|
+
body: { domain: "new-domain.com" },
|
|
831
|
+
headers: otherHeaders,
|
|
832
|
+
asResponse: true,
|
|
833
|
+
});
|
|
834
|
+
expect(response.status).toBe(403);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("should update domain and reset domainVerified to false", async () => {
|
|
838
|
+
const { auth, getAuthHeaders, registerSAMLProvider } = createTestAuth(
|
|
839
|
+
false,
|
|
840
|
+
true,
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
const headers = await getAuthHeaders({
|
|
844
|
+
email: "owner@example.com",
|
|
845
|
+
password: "password123",
|
|
846
|
+
name: "Owner",
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
850
|
+
|
|
851
|
+
const updated = await auth.api.updateSSOProvider({
|
|
852
|
+
params: { providerId: "my-saml-provider" },
|
|
853
|
+
body: { domain: "new-domain.com" },
|
|
854
|
+
headers,
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
expect(updated.domain).toBe("new-domain.com");
|
|
858
|
+
expect(updated.domainVerified).toBe(false);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it("should perform partial update on SAML provider", async () => {
|
|
862
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
863
|
+
createTestAuth(false);
|
|
864
|
+
|
|
865
|
+
const headers = await getAuthHeaders({
|
|
866
|
+
email: "owner@example.com",
|
|
867
|
+
password: "password123",
|
|
868
|
+
name: "Owner",
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
872
|
+
|
|
873
|
+
const updated = await auth.api.updateSSOProvider({
|
|
874
|
+
params: { providerId: "my-saml-provider" },
|
|
875
|
+
body: {
|
|
876
|
+
samlConfig: {
|
|
877
|
+
audience: "new-audience",
|
|
878
|
+
wantAssertionsSigned: false,
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
headers,
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
expect(updated.samlConfig?.audience).toBe("new-audience");
|
|
885
|
+
expect(updated.samlConfig?.wantAssertionsSigned).toBe(false);
|
|
886
|
+
expect(updated.samlConfig?.entryPoint).toBe(
|
|
887
|
+
"https://idp.example.com/sso",
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("should perform partial update on OIDC provider", async () => {
|
|
892
|
+
const { auth, getAuthHeaders, createOIDCProviderData, data } =
|
|
893
|
+
createTestAuth(false);
|
|
894
|
+
|
|
895
|
+
const headers = await getAuthHeaders({
|
|
896
|
+
email: "owner@example.com",
|
|
897
|
+
password: "password123",
|
|
898
|
+
name: "Owner",
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
902
|
+
(u) => u.email === "owner@example.com",
|
|
903
|
+
);
|
|
904
|
+
createOIDCProviderData(user!.id, "my-oidc-provider", "client123");
|
|
905
|
+
|
|
906
|
+
const updated = await auth.api.updateSSOProvider({
|
|
907
|
+
params: { providerId: "my-oidc-provider" },
|
|
908
|
+
body: {
|
|
909
|
+
oidcConfig: {
|
|
910
|
+
scopes: ["openid", "email", "profile", "custom"],
|
|
911
|
+
pkce: false,
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
headers,
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
expect(updated.oidcConfig?.scopes).toEqual([
|
|
918
|
+
"openid",
|
|
919
|
+
"email",
|
|
920
|
+
"profile",
|
|
921
|
+
"custom",
|
|
922
|
+
]);
|
|
923
|
+
expect(updated.oidcConfig?.pkce).toBe(false);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("should update issuer", async () => {
|
|
927
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
928
|
+
createTestAuth(false);
|
|
929
|
+
|
|
930
|
+
const headers = await getAuthHeaders({
|
|
931
|
+
email: "owner@example.com",
|
|
932
|
+
password: "password123",
|
|
933
|
+
name: "Owner",
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
937
|
+
|
|
938
|
+
const updated = await auth.api.updateSSOProvider({
|
|
939
|
+
params: { providerId: "my-saml-provider" },
|
|
940
|
+
body: { issuer: "https://new-issuer.example.com" },
|
|
941
|
+
headers,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
expect(updated.issuer).toBe("https://new-issuer.example.com");
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it("should return 400 when issuer is invalid URL", async () => {
|
|
948
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
949
|
+
createTestAuth(false);
|
|
950
|
+
|
|
951
|
+
const headers = await getAuthHeaders({
|
|
952
|
+
email: "owner@example.com",
|
|
953
|
+
password: "password123",
|
|
954
|
+
name: "Owner",
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
958
|
+
|
|
959
|
+
const response = await auth.api.updateSSOProvider({
|
|
960
|
+
params: { providerId: "my-saml-provider" },
|
|
961
|
+
body: { issuer: "invalid-url" },
|
|
962
|
+
headers,
|
|
963
|
+
asResponse: true,
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
expect(response.status).toBe(400);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("should return 400 when no fields provided", async () => {
|
|
970
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
971
|
+
createTestAuth(false);
|
|
972
|
+
|
|
973
|
+
const headers = await getAuthHeaders({
|
|
974
|
+
email: "owner@example.com",
|
|
975
|
+
password: "password123",
|
|
976
|
+
name: "Owner",
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
980
|
+
|
|
981
|
+
const response = await auth.api.updateSSOProvider({
|
|
982
|
+
params: { providerId: "my-saml-provider" },
|
|
983
|
+
body: {},
|
|
984
|
+
headers,
|
|
985
|
+
asResponse: true,
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
expect(response.status).toBe(400);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it("should allow org admin to update org provider", async () => {
|
|
992
|
+
const {
|
|
993
|
+
auth,
|
|
994
|
+
getAuthHeaders,
|
|
995
|
+
createOrganization,
|
|
996
|
+
registerSAMLProvider,
|
|
997
|
+
addMember,
|
|
998
|
+
data,
|
|
999
|
+
} = createTestAuth(true);
|
|
1000
|
+
|
|
1001
|
+
const ownerHeaders = await getAuthHeaders({
|
|
1002
|
+
email: "owner@example.com",
|
|
1003
|
+
password: "password123",
|
|
1004
|
+
name: "Owner",
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
const org = await createOrganization("test-org", ownerHeaders);
|
|
1008
|
+
await registerSAMLProvider(ownerHeaders, "org-provider", org!.id);
|
|
1009
|
+
|
|
1010
|
+
const adminHeaders = await getAuthHeaders({
|
|
1011
|
+
email: "admin@example.com",
|
|
1012
|
+
password: "password123",
|
|
1013
|
+
name: "Admin",
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
const adminUser = (data.user as { id: string; email: string }[]).find(
|
|
1017
|
+
(u) => u.email === "admin@example.com",
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
await addMember(adminUser!.id, org!.id, "admin", ownerHeaders);
|
|
1021
|
+
|
|
1022
|
+
const updated = await auth.api.updateSSOProvider({
|
|
1023
|
+
params: { providerId: "org-provider" },
|
|
1024
|
+
body: { domain: "new-domain.com" },
|
|
1025
|
+
headers: adminHeaders,
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
expect(updated.domain).toBe("new-domain.com");
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it("should return 403 when org member tries to update org provider", async () => {
|
|
1032
|
+
const {
|
|
1033
|
+
auth,
|
|
1034
|
+
getAuthHeaders,
|
|
1035
|
+
createOrganization,
|
|
1036
|
+
registerSAMLProvider,
|
|
1037
|
+
addMember,
|
|
1038
|
+
data,
|
|
1039
|
+
} = createTestAuth(true);
|
|
1040
|
+
|
|
1041
|
+
const ownerHeaders = await getAuthHeaders({
|
|
1042
|
+
email: "owner@example.com",
|
|
1043
|
+
password: "password123",
|
|
1044
|
+
name: "Owner",
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const org = await createOrganization("test-org", ownerHeaders);
|
|
1048
|
+
await registerSAMLProvider(ownerHeaders, "org-provider", org!.id);
|
|
1049
|
+
|
|
1050
|
+
const memberHeaders = await getAuthHeaders({
|
|
1051
|
+
email: "member@example.com",
|
|
1052
|
+
password: "password123",
|
|
1053
|
+
name: "Member",
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
const memberUser = (data.user as { id: string; email: string }[]).find(
|
|
1057
|
+
(u) => u.email === "member@example.com",
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
await addMember(memberUser!.id, org!.id, "member", ownerHeaders);
|
|
1061
|
+
|
|
1062
|
+
const response = await auth.api.updateSSOProvider({
|
|
1063
|
+
params: { providerId: "org-provider" },
|
|
1064
|
+
body: { domain: "new-domain.com" },
|
|
1065
|
+
headers: memberHeaders,
|
|
1066
|
+
asResponse: true,
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
expect(response.status).toBe(403);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it("should return 400 when trying to update SAML config for OIDC provider", async () => {
|
|
1073
|
+
const { auth, getAuthHeaders, createOIDCProviderData, data } =
|
|
1074
|
+
createTestAuth(false);
|
|
1075
|
+
|
|
1076
|
+
const headers = await getAuthHeaders({
|
|
1077
|
+
email: "owner@example.com",
|
|
1078
|
+
password: "password123",
|
|
1079
|
+
name: "Owner",
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
1083
|
+
(u) => u.email === "owner@example.com",
|
|
1084
|
+
);
|
|
1085
|
+
createOIDCProviderData(user!.id, "my-oidc-provider", "client123");
|
|
1086
|
+
|
|
1087
|
+
const response = await auth.api.updateSSOProvider({
|
|
1088
|
+
params: { providerId: "my-oidc-provider" },
|
|
1089
|
+
body: {
|
|
1090
|
+
samlConfig: {
|
|
1091
|
+
entryPoint: "https://idp.example.com/sso",
|
|
1092
|
+
cert: TEST_CERT,
|
|
1093
|
+
callbackUrl: "http://localhost:3000/api/sso/callback",
|
|
1094
|
+
spMetadata: {},
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
headers,
|
|
1098
|
+
asResponse: true,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
expect(response.status).toBe(400);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("should return 400 when trying to update OIDC config for SAML provider", async () => {
|
|
1105
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
1106
|
+
createTestAuth(false);
|
|
1107
|
+
|
|
1108
|
+
const headers = await getAuthHeaders({
|
|
1109
|
+
email: "owner@example.com",
|
|
1110
|
+
password: "password123",
|
|
1111
|
+
name: "Owner",
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
1115
|
+
|
|
1116
|
+
const response = await auth.api.updateSSOProvider({
|
|
1117
|
+
params: { providerId: "my-saml-provider" },
|
|
1118
|
+
body: {
|
|
1119
|
+
oidcConfig: {
|
|
1120
|
+
clientId: "new-client-id",
|
|
1121
|
+
clientSecret: "new-secret",
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
headers,
|
|
1125
|
+
asResponse: true,
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
expect(response.status).toBe(400);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
describe("DELETE /sso/providers/:providerId", () => {
|
|
1133
|
+
it("should return 401 when not authenticated", async () => {
|
|
1134
|
+
const { auth } = createTestAuth();
|
|
1135
|
+
const response = await auth.api.deleteSSOProvider({
|
|
1136
|
+
params: { providerId: "test" },
|
|
1137
|
+
asResponse: true,
|
|
1138
|
+
});
|
|
1139
|
+
expect(response.status).toBe(401);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it("should return 404 when provider not found", async () => {
|
|
1143
|
+
const { auth, getAuthHeaders } = createTestAuth();
|
|
1144
|
+
const headers = await getAuthHeaders({
|
|
1145
|
+
email: "test@example.com",
|
|
1146
|
+
password: "password123",
|
|
1147
|
+
name: "Test User",
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
const response = await auth.api.deleteSSOProvider({
|
|
1151
|
+
params: { providerId: "nonexistent" },
|
|
1152
|
+
headers,
|
|
1153
|
+
asResponse: true,
|
|
1154
|
+
});
|
|
1155
|
+
expect(response.status).toBe(404);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it("should return 403 when user does not own provider", async () => {
|
|
1159
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
1160
|
+
createTestAuth(false);
|
|
1161
|
+
|
|
1162
|
+
const ownerHeaders = await getAuthHeaders({
|
|
1163
|
+
email: "owner@example.com",
|
|
1164
|
+
password: "password123",
|
|
1165
|
+
name: "Owner",
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
await registerSAMLProvider(ownerHeaders, "other-provider");
|
|
1169
|
+
|
|
1170
|
+
const otherHeaders = await getAuthHeaders({
|
|
1171
|
+
email: "other@example.com",
|
|
1172
|
+
password: "password123",
|
|
1173
|
+
name: "Other User",
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
const response = await auth.api.deleteSSOProvider({
|
|
1177
|
+
params: { providerId: "other-provider" },
|
|
1178
|
+
headers: otherHeaders,
|
|
1179
|
+
asResponse: true,
|
|
1180
|
+
});
|
|
1181
|
+
expect(response.status).toBe(403);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it("should delete provider successfully", async () => {
|
|
1185
|
+
const { auth, getAuthHeaders, registerSAMLProvider } =
|
|
1186
|
+
createTestAuth(false);
|
|
1187
|
+
|
|
1188
|
+
const headers = await getAuthHeaders({
|
|
1189
|
+
email: "owner@example.com",
|
|
1190
|
+
password: "password123",
|
|
1191
|
+
name: "Owner",
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
1195
|
+
|
|
1196
|
+
const deleteResponse = await auth.api.deleteSSOProvider({
|
|
1197
|
+
params: { providerId: "my-saml-provider" },
|
|
1198
|
+
headers,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
expect(deleteResponse.success).toBe(true);
|
|
1202
|
+
|
|
1203
|
+
const getResponse = await auth.api.getSSOProvider({
|
|
1204
|
+
params: { providerId: "my-saml-provider" },
|
|
1205
|
+
headers,
|
|
1206
|
+
asResponse: true,
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
expect(getResponse.status).toBe(404);
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it("should allow org admin to delete org provider", async () => {
|
|
1213
|
+
const {
|
|
1214
|
+
auth,
|
|
1215
|
+
getAuthHeaders,
|
|
1216
|
+
createOrganization,
|
|
1217
|
+
registerSAMLProvider,
|
|
1218
|
+
addMember,
|
|
1219
|
+
data,
|
|
1220
|
+
} = createTestAuth(true);
|
|
1221
|
+
|
|
1222
|
+
const ownerHeaders = await getAuthHeaders({
|
|
1223
|
+
email: "owner@example.com",
|
|
1224
|
+
password: "password123",
|
|
1225
|
+
name: "Owner",
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
const org = await createOrganization("test-org", ownerHeaders);
|
|
1229
|
+
await registerSAMLProvider(ownerHeaders, "org-provider", org!.id);
|
|
1230
|
+
|
|
1231
|
+
const adminHeaders = await getAuthHeaders({
|
|
1232
|
+
email: "admin@example.com",
|
|
1233
|
+
password: "password123",
|
|
1234
|
+
name: "Admin",
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
const adminUser = (data.user as { id: string; email: string }[]).find(
|
|
1238
|
+
(u) => u.email === "admin@example.com",
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
await addMember(adminUser!.id, org!.id, "admin", ownerHeaders);
|
|
1242
|
+
|
|
1243
|
+
const deleteResponse = await auth.api.deleteSSOProvider({
|
|
1244
|
+
params: { providerId: "org-provider" },
|
|
1245
|
+
headers: adminHeaders,
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
expect(deleteResponse.success).toBe(true);
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it("should return 403 when org member tries to delete org provider", async () => {
|
|
1252
|
+
const {
|
|
1253
|
+
auth,
|
|
1254
|
+
getAuthHeaders,
|
|
1255
|
+
createOrganization,
|
|
1256
|
+
registerSAMLProvider,
|
|
1257
|
+
addMember,
|
|
1258
|
+
data,
|
|
1259
|
+
} = createTestAuth(true);
|
|
1260
|
+
|
|
1261
|
+
const ownerHeaders = await getAuthHeaders({
|
|
1262
|
+
email: "owner@example.com",
|
|
1263
|
+
password: "password123",
|
|
1264
|
+
name: "Owner",
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
const org = await createOrganization("test-org", ownerHeaders);
|
|
1268
|
+
await registerSAMLProvider(ownerHeaders, "org-provider", org!.id);
|
|
1269
|
+
|
|
1270
|
+
const memberHeaders = await getAuthHeaders({
|
|
1271
|
+
email: "member@example.com",
|
|
1272
|
+
password: "password123",
|
|
1273
|
+
name: "Member",
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
const memberUser = (data.user as { id: string; email: string }[]).find(
|
|
1277
|
+
(u) => u.email === "member@example.com",
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
await addMember(memberUser!.id, org!.id, "member", ownerHeaders);
|
|
1281
|
+
|
|
1282
|
+
const response = await auth.api.deleteSSOProvider({
|
|
1283
|
+
params: { providerId: "org-provider" },
|
|
1284
|
+
headers: memberHeaders,
|
|
1285
|
+
asResponse: true,
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
expect(response.status).toBe(403);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
it("should not delete linked accounts when provider is deleted", async () => {
|
|
1292
|
+
const { auth, getAuthHeaders, registerSAMLProvider, data } =
|
|
1293
|
+
createTestAuth(false);
|
|
1294
|
+
|
|
1295
|
+
const headers = await getAuthHeaders({
|
|
1296
|
+
email: "owner@example.com",
|
|
1297
|
+
password: "password123",
|
|
1298
|
+
name: "Owner",
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
await registerSAMLProvider(headers, "my-saml-provider");
|
|
1302
|
+
|
|
1303
|
+
const user = (data.user as { id: string; email: string }[]).find(
|
|
1304
|
+
(u) => u.email === "owner@example.com",
|
|
1305
|
+
);
|
|
1306
|
+
|
|
1307
|
+
data.account.push({
|
|
1308
|
+
id: "account-1",
|
|
1309
|
+
userId: user!.id,
|
|
1310
|
+
providerId: "my-saml-provider",
|
|
1311
|
+
accountId: "saml-account-id",
|
|
1312
|
+
accessToken: "token",
|
|
1313
|
+
refreshToken: "refresh",
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
const accountCountBefore = data.account.length;
|
|
1317
|
+
|
|
1318
|
+
await auth.api.deleteSSOProvider({
|
|
1319
|
+
params: { providerId: "my-saml-provider" },
|
|
1320
|
+
headers,
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
expect(data.account.length).toBe(accountCountBefore);
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
});
|