@better-auth/sso 1.4.8-beta.3 → 1.4.8-beta.6
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 +5 -5
- package/dist/client.d.mts +1 -1
- package/dist/{index-DNWhGQW-.d.mts → index-D6Q3ojGP.d.mts} +12 -12
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +12 -6
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/linking/org-assignment.test.ts +325 -0
- package/src/linking/org-assignment.ts +13 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.4.8-beta.
|
|
2
|
+
> @better-auth/sso@1.4.8-beta.6 build /home/runner/work/better-auth/better-auth/packages/sso
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.17.2[22m powered by rolldown [2mv1.0.0-beta.53[22m
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m92.
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m92.65 kB[22m [2m│ gzip: 18.13 kB[22m
|
|
11
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
12
12
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 1.48 kB[22m [2m│ gzip: 0.51 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.30 kB[22m
|
|
14
|
-
[34mℹ[39m [2mdist/[22m[32mindex-
|
|
15
|
-
[34mℹ[39m 5 files, total: 137.
|
|
16
|
-
[32m✔[39m Build complete in [
|
|
14
|
+
[34mℹ[39m [2mdist/[22m[32mindex-D6Q3ojGP.d.mts[39m [2m42.86 kB[22m [2m│ gzip: 8.79 kB[22m
|
|
15
|
+
[34mℹ[39m 5 files, total: 137.63 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m11966ms[39m
|
package/dist/client.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { APIError } from "better-auth/api";
|
|
|
2
2
|
import * as z$1 from "zod/v4";
|
|
3
3
|
import z from "zod/v4";
|
|
4
4
|
import { Awaitable, OAuth2Tokens, User } from "better-auth";
|
|
5
|
-
import * as
|
|
5
|
+
import * as better_call7 from "better-call";
|
|
6
6
|
|
|
7
7
|
//#region src/saml/algorithms.d.ts
|
|
8
8
|
declare const SignatureAlgorithm: {
|
|
@@ -371,7 +371,7 @@ interface SSOOptions {
|
|
|
371
371
|
}
|
|
372
372
|
//#endregion
|
|
373
373
|
//#region src/routes/domain-verification.d.ts
|
|
374
|
-
declare const requestDomainVerification: (options: SSOOptions) =>
|
|
374
|
+
declare const requestDomainVerification: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/request-domain-verification", {
|
|
375
375
|
method: "POST";
|
|
376
376
|
body: z$1.ZodObject<{
|
|
377
377
|
providerId: z$1.ZodString;
|
|
@@ -393,7 +393,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call0.S
|
|
|
393
393
|
};
|
|
394
394
|
};
|
|
395
395
|
};
|
|
396
|
-
use: ((inputContext:
|
|
396
|
+
use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
|
|
397
397
|
session: {
|
|
398
398
|
session: Record<string, any> & {
|
|
399
399
|
id: string;
|
|
@@ -419,7 +419,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call0.S
|
|
|
419
419
|
}, {
|
|
420
420
|
domainVerificationToken: string;
|
|
421
421
|
}>;
|
|
422
|
-
declare const verifyDomain: (options: SSOOptions) =>
|
|
422
|
+
declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/verify-domain", {
|
|
423
423
|
method: "POST";
|
|
424
424
|
body: z$1.ZodObject<{
|
|
425
425
|
providerId: z$1.ZodString;
|
|
@@ -444,7 +444,7 @@ declare const verifyDomain: (options: SSOOptions) => better_call0.StrictEndpoint
|
|
|
444
444
|
};
|
|
445
445
|
};
|
|
446
446
|
};
|
|
447
|
-
use: ((inputContext:
|
|
447
|
+
use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
|
|
448
448
|
session: {
|
|
449
449
|
session: Record<string, any> & {
|
|
450
450
|
id: string;
|
|
@@ -488,7 +488,7 @@ interface SAMLConditions {
|
|
|
488
488
|
* @throws {APIError} If timestamps are invalid, expired, or not yet valid
|
|
489
489
|
*/
|
|
490
490
|
declare function validateSAMLTimestamp(conditions: SAMLConditions | undefined, options?: TimestampValidationOptions): void;
|
|
491
|
-
declare const spMetadata: () =>
|
|
491
|
+
declare const spMetadata: () => better_call7.StrictEndpoint<"/sso/saml2/sp/metadata", {
|
|
492
492
|
method: "GET";
|
|
493
493
|
query: z.ZodObject<{
|
|
494
494
|
providerId: z.ZodString;
|
|
@@ -510,7 +510,7 @@ declare const spMetadata: () => better_call0.StrictEndpoint<"/sso/saml2/sp/metad
|
|
|
510
510
|
};
|
|
511
511
|
};
|
|
512
512
|
}, Response>;
|
|
513
|
-
declare const registerSSOProvider: <O extends SSOOptions>(options: O) =>
|
|
513
|
+
declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_call7.StrictEndpoint<"/sso/register", {
|
|
514
514
|
method: "POST";
|
|
515
515
|
body: z.ZodObject<{
|
|
516
516
|
providerId: z.ZodString;
|
|
@@ -589,7 +589,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
|
|
|
589
589
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
590
590
|
overrideUserInfo: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
591
591
|
}, z.core.$strip>;
|
|
592
|
-
use: ((inputContext:
|
|
592
|
+
use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
|
|
593
593
|
session: {
|
|
594
594
|
session: Record<string, any> & {
|
|
595
595
|
id: string;
|
|
@@ -779,7 +779,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
|
|
|
779
779
|
domainVerified: boolean;
|
|
780
780
|
domainVerificationToken: string;
|
|
781
781
|
} & SSOProvider<O> : SSOProvider<O>>;
|
|
782
|
-
declare const signInSSO: (options?: SSOOptions) =>
|
|
782
|
+
declare const signInSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sign-in/sso", {
|
|
783
783
|
method: "POST";
|
|
784
784
|
body: z.ZodObject<{
|
|
785
785
|
email: z.ZodOptional<z.ZodString>;
|
|
@@ -873,7 +873,7 @@ declare const signInSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"
|
|
|
873
873
|
url: string;
|
|
874
874
|
redirect: boolean;
|
|
875
875
|
}>;
|
|
876
|
-
declare const callbackSSO: (options?: SSOOptions) =>
|
|
876
|
+
declare const callbackSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/callback/:providerId", {
|
|
877
877
|
method: "GET";
|
|
878
878
|
query: z.ZodObject<{
|
|
879
879
|
code: z.ZodOptional<z.ZodString>;
|
|
@@ -896,7 +896,7 @@ declare const callbackSSO: (options?: SSOOptions) => better_call0.StrictEndpoint
|
|
|
896
896
|
scope: "server";
|
|
897
897
|
};
|
|
898
898
|
}, never>;
|
|
899
|
-
declare const callbackSSOSAML: (options?: SSOOptions) =>
|
|
899
|
+
declare const callbackSSOSAML: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/callback/:providerId", {
|
|
900
900
|
method: "POST";
|
|
901
901
|
body: z.ZodObject<{
|
|
902
902
|
SAMLResponse: z.ZodString;
|
|
@@ -923,7 +923,7 @@ declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndp
|
|
|
923
923
|
scope: "server";
|
|
924
924
|
};
|
|
925
925
|
}, never>;
|
|
926
|
-
declare const acsEndpoint: (options?: SSOOptions) =>
|
|
926
|
+
declare const acsEndpoint: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
|
|
927
927
|
method: "POST";
|
|
928
928
|
params: z.ZodObject<{
|
|
929
929
|
providerId: z.ZodOptional<z.ZodString>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as KeyEncryptionAlgorithm, C as SAMLConfig, D as DataEncryptionAlgorithm, E as AlgorithmValidationOptions, O as DeprecatedAlgorithmBehavior, S as OIDCConfig, T as SSOProvider, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SignatureAlgorithm, k as DigestAlgorithm, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as SSOOptions, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-
|
|
1
|
+
import { A as KeyEncryptionAlgorithm, C as SAMLConfig, D as DataEncryptionAlgorithm, E as AlgorithmValidationOptions, O as DeprecatedAlgorithmBehavior, S as OIDCConfig, T as SSOProvider, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SignatureAlgorithm, k as DigestAlgorithm, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as SSOOptions, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-D6Q3ojGP.mjs";
|
|
2
2
|
export { AlgorithmValidationOptions, DataEncryptionAlgorithm, DeprecatedAlgorithmBehavior, DigestAlgorithm, DiscoverOIDCConfigParams, DiscoveryError, DiscoveryErrorCode, HydratedOIDCConfig, KeyEncryptionAlgorithm, OIDCConfig, OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, RequiredDiscoveryField, SAMLConditions, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, SignatureAlgorithm, TimestampValidationOptions, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
|
package/dist/index.mjs
CHANGED
|
@@ -55,17 +55,22 @@ async function assignOrganizationFromProvider(ctx, options) {
|
|
|
55
55
|
* (e.g., Google OAuth with @acme.com email gets added to Acme's org).
|
|
56
56
|
*/
|
|
57
57
|
async function assignOrganizationByDomain(ctx, options) {
|
|
58
|
-
const { user, provisioningOptions } = options;
|
|
58
|
+
const { user, provisioningOptions, domainVerification } = options;
|
|
59
59
|
if (provisioningOptions?.disabled) return;
|
|
60
60
|
if (!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) return;
|
|
61
61
|
const domain = user.email.split("@")[1];
|
|
62
62
|
if (!domain) return;
|
|
63
|
+
const whereClause = [{
|
|
64
|
+
field: "domain",
|
|
65
|
+
value: domain
|
|
66
|
+
}];
|
|
67
|
+
if (domainVerification?.enabled) whereClause.push({
|
|
68
|
+
field: "domainVerified",
|
|
69
|
+
value: true
|
|
70
|
+
});
|
|
63
71
|
const ssoProvider = await ctx.context.adapter.findOne({
|
|
64
72
|
model: "ssoProvider",
|
|
65
|
-
where:
|
|
66
|
-
field: "domain",
|
|
67
|
-
value: domain
|
|
68
|
-
}]
|
|
73
|
+
where: whereClause
|
|
69
74
|
});
|
|
70
75
|
if (!ssoProvider || !ssoProvider.organizationId) return;
|
|
71
76
|
if (await ctx.context.adapter.findOne({
|
|
@@ -2203,7 +2208,8 @@ function sso(options) {
|
|
|
2203
2208
|
if (!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) return;
|
|
2204
2209
|
await assignOrganizationByDomain(ctx, {
|
|
2205
2210
|
user: newSession.user,
|
|
2206
|
-
provisioningOptions: options?.organizationProvisioning
|
|
2211
|
+
provisioningOptions: options?.organizationProvisioning,
|
|
2212
|
+
domainVerification: options?.domainVerification
|
|
2207
2213
|
});
|
|
2208
2214
|
})
|
|
2209
2215
|
}] },
|
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.8-beta.
|
|
4
|
+
"version": "1.4.8-beta.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -61,15 +61,15 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/body-parser": "^1.19.6",
|
|
63
63
|
"@types/express": "^5.0.5",
|
|
64
|
-
"better-call": "1.1.
|
|
64
|
+
"better-call": "1.1.7",
|
|
65
65
|
"body-parser": "^2.2.1",
|
|
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.8-beta.
|
|
69
|
+
"better-auth": "1.4.8-beta.6"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"better-auth": "1.4.8-beta.
|
|
72
|
+
"better-auth": "1.4.8-beta.6"
|
|
73
73
|
},
|
|
74
74
|
"scripts": {
|
|
75
75
|
"test": "vitest",
|
package/src/index.ts
CHANGED
|
@@ -156,6 +156,7 @@ export function sso<O extends SSOOptions>(options?: O | undefined): any {
|
|
|
156
156
|
await assignOrganizationByDomain(ctx, {
|
|
157
157
|
user: newSession.user,
|
|
158
158
|
provisioningOptions: options?.organizationProvisioning,
|
|
159
|
+
domainVerification: options?.domainVerification,
|
|
159
160
|
});
|
|
160
161
|
}),
|
|
161
162
|
},
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { GenericEndpointContext, User } from "better-auth";
|
|
2
|
+
import { betterAuth } from "better-auth";
|
|
3
|
+
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
4
|
+
import { organization } from "better-auth/plugins";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { sso } from "..";
|
|
7
|
+
import { assignOrganizationByDomain } from "./org-assignment";
|
|
8
|
+
|
|
9
|
+
describe("assignOrganizationByDomain", () => {
|
|
10
|
+
const createTestContext = () => {
|
|
11
|
+
const data = {
|
|
12
|
+
user: [] as User[],
|
|
13
|
+
session: [] as { id: string }[],
|
|
14
|
+
account: [] as { id: string }[],
|
|
15
|
+
ssoProvider: [] as {
|
|
16
|
+
id: string;
|
|
17
|
+
providerId: string;
|
|
18
|
+
issuer: string;
|
|
19
|
+
domain: string;
|
|
20
|
+
domainVerified: boolean;
|
|
21
|
+
organizationId: string | null;
|
|
22
|
+
userId: string;
|
|
23
|
+
}[],
|
|
24
|
+
member: [] as {
|
|
25
|
+
id: string;
|
|
26
|
+
organizationId: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
role: string;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
}[],
|
|
31
|
+
organization: [] as {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
createdAt: Date;
|
|
36
|
+
}[],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const memory = memoryAdapter(data);
|
|
40
|
+
|
|
41
|
+
const auth = betterAuth({
|
|
42
|
+
database: memory,
|
|
43
|
+
baseURL: "http://localhost:3000",
|
|
44
|
+
emailAndPassword: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
},
|
|
47
|
+
plugins: [
|
|
48
|
+
sso({
|
|
49
|
+
domainVerification: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
organization(),
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const createContext = async () => {
|
|
58
|
+
const context = await auth.$context;
|
|
59
|
+
return { context } as Partial<GenericEndpointContext>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return { auth, data, createContext };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const createUser = (overrides: Partial<User> = {}): User => ({
|
|
66
|
+
id: "user-1",
|
|
67
|
+
email: "alice@example.com",
|
|
68
|
+
name: "Alice",
|
|
69
|
+
emailVerified: true,
|
|
70
|
+
createdAt: new Date(),
|
|
71
|
+
updatedAt: new Date(),
|
|
72
|
+
...overrides,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const createOrg = (
|
|
76
|
+
overrides: Partial<{ id: string; name: string; slug: string }> = {},
|
|
77
|
+
) => ({
|
|
78
|
+
id: "org-1",
|
|
79
|
+
name: "Test Org",
|
|
80
|
+
slug: "test-org",
|
|
81
|
+
createdAt: new Date(),
|
|
82
|
+
...overrides,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const createProvider = (
|
|
86
|
+
overrides: Partial<{
|
|
87
|
+
id: string;
|
|
88
|
+
providerId: string;
|
|
89
|
+
issuer: string;
|
|
90
|
+
domain: string;
|
|
91
|
+
domainVerified: boolean;
|
|
92
|
+
organizationId: string | null;
|
|
93
|
+
userId: string;
|
|
94
|
+
}> = {},
|
|
95
|
+
) => ({
|
|
96
|
+
id: "provider-1",
|
|
97
|
+
providerId: "test-provider",
|
|
98
|
+
issuer: "https://idp.example.com",
|
|
99
|
+
domain: "example.com",
|
|
100
|
+
domainVerified: false,
|
|
101
|
+
organizationId: "org-1" as string | null,
|
|
102
|
+
userId: "user-1",
|
|
103
|
+
...overrides,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should NOT assign user to org when provider domain is unverified", async () => {
|
|
107
|
+
const { data, createContext } = createTestContext();
|
|
108
|
+
|
|
109
|
+
data.organization.push(createOrg());
|
|
110
|
+
data.ssoProvider.push(createProvider({ domainVerified: false }));
|
|
111
|
+
|
|
112
|
+
const user = createUser();
|
|
113
|
+
data.user.push(user);
|
|
114
|
+
|
|
115
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
116
|
+
await assignOrganizationByDomain(ctx, {
|
|
117
|
+
user,
|
|
118
|
+
domainVerification: { enabled: true },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
122
|
+
expect(members).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should assign user to org when provider domain is verified", async () => {
|
|
126
|
+
const { data, createContext } = createTestContext();
|
|
127
|
+
|
|
128
|
+
const org = createOrg();
|
|
129
|
+
data.organization.push(org);
|
|
130
|
+
data.ssoProvider.push(
|
|
131
|
+
createProvider({ domainVerified: true, organizationId: org.id }),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const user = createUser();
|
|
135
|
+
data.user.push(user);
|
|
136
|
+
|
|
137
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
138
|
+
await assignOrganizationByDomain(ctx, {
|
|
139
|
+
user,
|
|
140
|
+
domainVerification: { enabled: true },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
144
|
+
expect(members).toHaveLength(1);
|
|
145
|
+
expect(members[0]?.organizationId).toBe(org.id);
|
|
146
|
+
expect(members[0]?.role).toBe("member");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should NOT assign user when email domain does not match any provider", async () => {
|
|
150
|
+
const { data, createContext } = createTestContext();
|
|
151
|
+
|
|
152
|
+
data.organization.push(createOrg());
|
|
153
|
+
data.ssoProvider.push(createProvider({ domainVerified: true }));
|
|
154
|
+
|
|
155
|
+
const user = createUser({ email: "alice@other-domain.com" });
|
|
156
|
+
data.user.push(user);
|
|
157
|
+
|
|
158
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
159
|
+
await assignOrganizationByDomain(ctx, {
|
|
160
|
+
user,
|
|
161
|
+
domainVerification: { enabled: true },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
165
|
+
expect(members).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should NOT assign user when provider has no organizationId", async () => {
|
|
169
|
+
const { data, createContext } = createTestContext();
|
|
170
|
+
|
|
171
|
+
data.ssoProvider.push(
|
|
172
|
+
createProvider({ domainVerified: true, organizationId: null }),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const user = createUser();
|
|
176
|
+
data.user.push(user);
|
|
177
|
+
|
|
178
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
179
|
+
await assignOrganizationByDomain(ctx, {
|
|
180
|
+
user,
|
|
181
|
+
domainVerification: { enabled: true },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
185
|
+
expect(members).toHaveLength(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should NOT assign user when provider has no domainVerified field (verification enabled)", async () => {
|
|
189
|
+
const { data, createContext } = createTestContext();
|
|
190
|
+
|
|
191
|
+
const org = createOrg();
|
|
192
|
+
data.organization.push(org);
|
|
193
|
+
|
|
194
|
+
data.ssoProvider.push({
|
|
195
|
+
id: "provider-1",
|
|
196
|
+
providerId: "test-provider",
|
|
197
|
+
issuer: "https://idp.example.com",
|
|
198
|
+
domain: "example.com",
|
|
199
|
+
organizationId: org.id,
|
|
200
|
+
userId: "user-1",
|
|
201
|
+
} as {
|
|
202
|
+
id: string;
|
|
203
|
+
providerId: string;
|
|
204
|
+
issuer: string;
|
|
205
|
+
domain: string;
|
|
206
|
+
domainVerified: boolean;
|
|
207
|
+
organizationId: string | null;
|
|
208
|
+
userId: string;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const user = createUser();
|
|
212
|
+
data.user.push(user);
|
|
213
|
+
|
|
214
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
215
|
+
await assignOrganizationByDomain(ctx, {
|
|
216
|
+
user,
|
|
217
|
+
domainVerification: { enabled: true },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
221
|
+
expect(members).toHaveLength(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should assign user when verification is disabled (no domainVerified check)", async () => {
|
|
225
|
+
const { data, createContext } = createTestContext();
|
|
226
|
+
|
|
227
|
+
const org = createOrg();
|
|
228
|
+
data.organization.push(org);
|
|
229
|
+
data.ssoProvider.push(
|
|
230
|
+
createProvider({ domainVerified: false, organizationId: org.id }),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const user = createUser();
|
|
234
|
+
data.user.push(user);
|
|
235
|
+
|
|
236
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
237
|
+
await assignOrganizationByDomain(ctx, {
|
|
238
|
+
user,
|
|
239
|
+
domainVerification: { enabled: false },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
243
|
+
expect(members).toHaveLength(1);
|
|
244
|
+
expect(members[0]?.organizationId).toBe(org.id);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should NOT assign user when already a member of the org", async () => {
|
|
248
|
+
const { data, createContext } = createTestContext();
|
|
249
|
+
|
|
250
|
+
const org = createOrg();
|
|
251
|
+
data.organization.push(org);
|
|
252
|
+
data.ssoProvider.push(
|
|
253
|
+
createProvider({ domainVerified: true, organizationId: org.id }),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const user = createUser();
|
|
257
|
+
data.user.push(user);
|
|
258
|
+
|
|
259
|
+
data.member.push({
|
|
260
|
+
id: "member-1",
|
|
261
|
+
organizationId: org.id,
|
|
262
|
+
userId: user.id,
|
|
263
|
+
role: "admin",
|
|
264
|
+
createdAt: new Date(),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
268
|
+
await assignOrganizationByDomain(ctx, {
|
|
269
|
+
user,
|
|
270
|
+
domainVerification: { enabled: true },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
274
|
+
expect(members).toHaveLength(1);
|
|
275
|
+
expect(members[0]?.role).toBe("admin");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should only find verified provider when multiple providers claim same domain", async () => {
|
|
279
|
+
const { data, createContext } = createTestContext();
|
|
280
|
+
|
|
281
|
+
const legitOrg = createOrg({
|
|
282
|
+
id: "legit-org",
|
|
283
|
+
name: "Legit Org",
|
|
284
|
+
slug: "legit-org",
|
|
285
|
+
});
|
|
286
|
+
const attackerOrg = createOrg({
|
|
287
|
+
id: "attacker-org",
|
|
288
|
+
name: "Attacker Org",
|
|
289
|
+
slug: "attacker-org",
|
|
290
|
+
});
|
|
291
|
+
data.organization.push(legitOrg, attackerOrg);
|
|
292
|
+
|
|
293
|
+
data.ssoProvider.push(
|
|
294
|
+
createProvider({
|
|
295
|
+
id: "attacker-provider",
|
|
296
|
+
providerId: "attacker-provider",
|
|
297
|
+
issuer: "https://attacker.com",
|
|
298
|
+
domainVerified: false,
|
|
299
|
+
organizationId: attackerOrg.id,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
data.ssoProvider.push(
|
|
304
|
+
createProvider({
|
|
305
|
+
id: "legit-provider",
|
|
306
|
+
providerId: "legit-provider",
|
|
307
|
+
domainVerified: true,
|
|
308
|
+
organizationId: legitOrg.id,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const user = createUser();
|
|
313
|
+
data.user.push(user);
|
|
314
|
+
|
|
315
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
316
|
+
await assignOrganizationByDomain(ctx, {
|
|
317
|
+
user,
|
|
318
|
+
domainVerification: { enabled: true },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
322
|
+
expect(members).toHaveLength(1);
|
|
323
|
+
expect(members[0]?.organizationId).toBe(legitOrg.id);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -82,6 +82,9 @@ export async function assignOrganizationFromProvider(
|
|
|
82
82
|
export interface AssignOrganizationByDomainOptions {
|
|
83
83
|
user: User;
|
|
84
84
|
provisioningOptions?: OrganizationProvisioningOptions;
|
|
85
|
+
domainVerification?: {
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
};
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -96,7 +99,7 @@ export async function assignOrganizationByDomain(
|
|
|
96
99
|
ctx: GenericEndpointContext,
|
|
97
100
|
options: AssignOrganizationByDomainOptions,
|
|
98
101
|
): Promise<void> {
|
|
99
|
-
const { user, provisioningOptions } = options;
|
|
102
|
+
const { user, provisioningOptions, domainVerification } = options;
|
|
100
103
|
|
|
101
104
|
if (provisioningOptions?.disabled) {
|
|
102
105
|
return;
|
|
@@ -115,11 +118,19 @@ export async function assignOrganizationByDomain(
|
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
const whereClause: { field: string; value: string | boolean }[] = [
|
|
122
|
+
{ field: "domain", value: domain },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
if (domainVerification?.enabled) {
|
|
126
|
+
whereClause.push({ field: "domainVerified", value: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
const ssoProvider = await ctx.context.adapter.findOne<
|
|
119
130
|
SSOProvider<SSOOptions>
|
|
120
131
|
>({
|
|
121
132
|
model: "ssoProvider",
|
|
122
|
-
where:
|
|
133
|
+
where: whereClause,
|
|
123
134
|
});
|
|
124
135
|
|
|
125
136
|
if (!ssoProvider || !ssoProvider.organizationId) {
|