@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { BetterAuthPlugin } from "better-auth";
|
|
2
|
+
import { createAuthMiddleware } from "better-auth/api";
|
|
3
|
+
import { XMLValidator } from "fast-xml-parser";
|
|
4
|
+
import * as saml from "samlify";
|
|
5
|
+
import { assignOrganizationByDomain } from "./linking";
|
|
6
|
+
import {
|
|
7
|
+
requestDomainVerification,
|
|
8
|
+
verifyDomain,
|
|
9
|
+
} from "./routes/domain-verification";
|
|
10
|
+
import {
|
|
11
|
+
deleteSSOProvider,
|
|
12
|
+
getSSOProvider,
|
|
13
|
+
listSSOProviders,
|
|
14
|
+
updateSSOProvider,
|
|
15
|
+
} from "./routes/providers";
|
|
16
|
+
import {
|
|
17
|
+
acsEndpoint,
|
|
18
|
+
callbackSSO,
|
|
19
|
+
callbackSSOSAML,
|
|
20
|
+
registerSSOProvider,
|
|
21
|
+
signInSSO,
|
|
22
|
+
spMetadata,
|
|
23
|
+
} from "./routes/sso";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_CLOCK_SKEW_MS,
|
|
27
|
+
DEFAULT_MAX_SAML_METADATA_SIZE,
|
|
28
|
+
DEFAULT_MAX_SAML_RESPONSE_SIZE,
|
|
29
|
+
} from "./constants";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
type SAMLConditions,
|
|
33
|
+
type TimestampValidationOptions,
|
|
34
|
+
validateSAMLTimestamp,
|
|
35
|
+
} from "./routes/sso";
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
type AlgorithmValidationOptions,
|
|
39
|
+
DataEncryptionAlgorithm,
|
|
40
|
+
type DeprecatedAlgorithmBehavior,
|
|
41
|
+
DigestAlgorithm,
|
|
42
|
+
KeyEncryptionAlgorithm,
|
|
43
|
+
SignatureAlgorithm,
|
|
44
|
+
} from "./saml";
|
|
45
|
+
|
|
46
|
+
import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "./types";
|
|
47
|
+
|
|
48
|
+
export type { SAMLConfig, OIDCConfig, SSOOptions, SSOProvider };
|
|
49
|
+
|
|
50
|
+
declare module "@better-auth/core" {
|
|
51
|
+
interface BetterAuthPluginRegistry<AuthOptions, Options> {
|
|
52
|
+
sso: {
|
|
53
|
+
creator: typeof sso;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
computeDiscoveryUrl,
|
|
60
|
+
type DiscoverOIDCConfigParams,
|
|
61
|
+
DiscoveryError,
|
|
62
|
+
type DiscoveryErrorCode,
|
|
63
|
+
discoverOIDCConfig,
|
|
64
|
+
fetchDiscoveryDocument,
|
|
65
|
+
type HydratedOIDCConfig,
|
|
66
|
+
needsRuntimeDiscovery,
|
|
67
|
+
normalizeDiscoveryUrls,
|
|
68
|
+
normalizeUrl,
|
|
69
|
+
type OIDCDiscoveryDocument,
|
|
70
|
+
REQUIRED_DISCOVERY_FIELDS,
|
|
71
|
+
type RequiredDiscoveryField,
|
|
72
|
+
selectTokenEndpointAuthMethod,
|
|
73
|
+
validateDiscoveryDocument,
|
|
74
|
+
validateDiscoveryUrl,
|
|
75
|
+
} from "./oidc";
|
|
76
|
+
|
|
77
|
+
const fastValidator = {
|
|
78
|
+
async validate(xml: string) {
|
|
79
|
+
const isValid = XMLValidator.validate(xml, {
|
|
80
|
+
allowBooleanAttributes: true,
|
|
81
|
+
});
|
|
82
|
+
if (isValid === true) return "SUCCESS_VALIDATE_XML";
|
|
83
|
+
throw "ERR_INVALID_XML";
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
saml.setSchemaValidator(fastValidator);
|
|
88
|
+
|
|
89
|
+
type DomainVerificationEndpoints = {
|
|
90
|
+
requestDomainVerification: ReturnType<typeof requestDomainVerification>;
|
|
91
|
+
verifyDomain: ReturnType<typeof verifyDomain>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type SSOEndpoints<O extends SSOOptions> = {
|
|
95
|
+
spMetadata: ReturnType<typeof spMetadata>;
|
|
96
|
+
registerSSOProvider: ReturnType<typeof registerSSOProvider<O>>;
|
|
97
|
+
signInSSO: ReturnType<typeof signInSSO>;
|
|
98
|
+
callbackSSO: ReturnType<typeof callbackSSO>;
|
|
99
|
+
callbackSSOSAML: ReturnType<typeof callbackSSOSAML>;
|
|
100
|
+
acsEndpoint: ReturnType<typeof acsEndpoint>;
|
|
101
|
+
listSSOProviders: ReturnType<typeof listSSOProviders>;
|
|
102
|
+
getSSOProvider: ReturnType<typeof getSSOProvider>;
|
|
103
|
+
updateSSOProvider: ReturnType<typeof updateSSOProvider>;
|
|
104
|
+
deleteSSOProvider: ReturnType<typeof deleteSSOProvider>;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type SSOPlugin<O extends SSOOptions> = {
|
|
108
|
+
id: "sso";
|
|
109
|
+
endpoints: SSOEndpoints<O> &
|
|
110
|
+
(O extends { domainVerification: { enabled: true } }
|
|
111
|
+
? DomainVerificationEndpoints
|
|
112
|
+
: {});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* SAML endpoint paths that should skip origin check validation.
|
|
117
|
+
* These endpoints receive POST requests from external Identity Providers,
|
|
118
|
+
* which won't have a matching Origin header.
|
|
119
|
+
*/
|
|
120
|
+
const SAML_SKIP_ORIGIN_CHECK_PATHS = [
|
|
121
|
+
"/sso/saml2/callback", // SP-initiated SSO callback (prefix matches /callback/:providerId)
|
|
122
|
+
"/sso/saml2/sp/acs", // IdP-initiated SSO ACS (prefix matches /sp/acs/:providerId)
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
export function sso<
|
|
126
|
+
O extends SSOOptions & {
|
|
127
|
+
domainVerification?: { enabled: true };
|
|
128
|
+
},
|
|
129
|
+
>(
|
|
130
|
+
options?: O | undefined,
|
|
131
|
+
): {
|
|
132
|
+
id: "sso";
|
|
133
|
+
endpoints: SSOEndpoints<O> & DomainVerificationEndpoints;
|
|
134
|
+
schema: NonNullable<BetterAuthPlugin["schema"]>;
|
|
135
|
+
options: O;
|
|
136
|
+
};
|
|
137
|
+
export function sso<O extends SSOOptions>(
|
|
138
|
+
options?: O | undefined,
|
|
139
|
+
): {
|
|
140
|
+
id: "sso";
|
|
141
|
+
endpoints: SSOEndpoints<O>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export function sso<O extends SSOOptions>(
|
|
145
|
+
options?: O | undefined,
|
|
146
|
+
): BetterAuthPlugin {
|
|
147
|
+
const optionsWithStore = options as O;
|
|
148
|
+
|
|
149
|
+
let endpoints = {
|
|
150
|
+
spMetadata: spMetadata(),
|
|
151
|
+
registerSSOProvider: registerSSOProvider(optionsWithStore),
|
|
152
|
+
signInSSO: signInSSO(optionsWithStore),
|
|
153
|
+
callbackSSO: callbackSSO(optionsWithStore),
|
|
154
|
+
callbackSSOSAML: callbackSSOSAML(optionsWithStore),
|
|
155
|
+
acsEndpoint: acsEndpoint(optionsWithStore),
|
|
156
|
+
listSSOProviders: listSSOProviders(),
|
|
157
|
+
getSSOProvider: getSSOProvider(),
|
|
158
|
+
updateSSOProvider: updateSSOProvider(optionsWithStore),
|
|
159
|
+
deleteSSOProvider: deleteSSOProvider(),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (options?.domainVerification?.enabled) {
|
|
163
|
+
const domainVerificationEndpoints = {
|
|
164
|
+
requestDomainVerification: requestDomainVerification(optionsWithStore),
|
|
165
|
+
verifyDomain: verifyDomain(optionsWithStore),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
endpoints = {
|
|
169
|
+
...endpoints,
|
|
170
|
+
...domainVerificationEndpoints,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: "sso",
|
|
176
|
+
init(ctx) {
|
|
177
|
+
const existing = ctx.skipOriginCheck;
|
|
178
|
+
if (existing === true) {
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
const existingPaths = Array.isArray(existing) ? existing : [];
|
|
182
|
+
return {
|
|
183
|
+
context: {
|
|
184
|
+
skipOriginCheck: [...existingPaths, ...SAML_SKIP_ORIGIN_CHECK_PATHS],
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
endpoints,
|
|
189
|
+
hooks: {
|
|
190
|
+
after: [
|
|
191
|
+
{
|
|
192
|
+
matcher(context) {
|
|
193
|
+
return context.path?.startsWith("/callback/") ?? false;
|
|
194
|
+
},
|
|
195
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
196
|
+
const newSession = ctx.context.newSession;
|
|
197
|
+
if (!newSession?.user) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!ctx.context.hasPlugin("organization")) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await assignOrganizationByDomain(ctx, {
|
|
206
|
+
user: newSession.user,
|
|
207
|
+
provisioningOptions: options?.organizationProvisioning,
|
|
208
|
+
domainVerification: options?.domainVerification,
|
|
209
|
+
});
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
schema: {
|
|
215
|
+
ssoProvider: {
|
|
216
|
+
modelName: options?.modelName ?? "ssoProvider",
|
|
217
|
+
fields: {
|
|
218
|
+
issuer: {
|
|
219
|
+
type: "string",
|
|
220
|
+
required: true,
|
|
221
|
+
fieldName: options?.fields?.issuer ?? "issuer",
|
|
222
|
+
},
|
|
223
|
+
oidcConfig: {
|
|
224
|
+
type: "string",
|
|
225
|
+
required: false,
|
|
226
|
+
fieldName: options?.fields?.oidcConfig ?? "oidcConfig",
|
|
227
|
+
},
|
|
228
|
+
samlConfig: {
|
|
229
|
+
type: "string",
|
|
230
|
+
required: false,
|
|
231
|
+
fieldName: options?.fields?.samlConfig ?? "samlConfig",
|
|
232
|
+
},
|
|
233
|
+
userId: {
|
|
234
|
+
type: "string",
|
|
235
|
+
references: {
|
|
236
|
+
model: "user",
|
|
237
|
+
field: "id",
|
|
238
|
+
},
|
|
239
|
+
fieldName: options?.fields?.userId ?? "userId",
|
|
240
|
+
},
|
|
241
|
+
providerId: {
|
|
242
|
+
type: "string",
|
|
243
|
+
required: true,
|
|
244
|
+
unique: true,
|
|
245
|
+
fieldName: options?.fields?.providerId ?? "providerId",
|
|
246
|
+
},
|
|
247
|
+
organizationId: {
|
|
248
|
+
type: "string",
|
|
249
|
+
required: false,
|
|
250
|
+
fieldName: options?.fields?.organizationId ?? "organizationId",
|
|
251
|
+
},
|
|
252
|
+
domain: {
|
|
253
|
+
type: "string",
|
|
254
|
+
required: true,
|
|
255
|
+
fieldName: options?.fields?.domain ?? "domain",
|
|
256
|
+
},
|
|
257
|
+
...(options?.domainVerification?.enabled
|
|
258
|
+
? { domainVerified: { type: "boolean", required: false } }
|
|
259
|
+
: {}),
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
options: options as NoInfer<O>,
|
|
264
|
+
} satisfies BetterAuthPlugin;
|
|
265
|
+
}
|
|
@@ -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 unknown 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
|
+
});
|