@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,176 @@
|
|
|
1
|
+
import type { GenericEndpointContext, OAuth2Tokens, User } from "better-auth";
|
|
2
|
+
import type { SSOOptions, SSOProvider } from "../types";
|
|
3
|
+
import { domainMatches } from "../utils";
|
|
4
|
+
import type { NormalizedSSOProfile } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface OrganizationProvisioningOptions {
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
defaultRole?: "member" | "admin";
|
|
9
|
+
getRole?: (data: {
|
|
10
|
+
user: User & Record<string, any>;
|
|
11
|
+
userInfo: Record<string, any>;
|
|
12
|
+
token?: OAuth2Tokens;
|
|
13
|
+
provider: SSOProvider<SSOOptions>;
|
|
14
|
+
}) => Promise<"member" | "admin">;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AssignOrganizationFromProviderOptions {
|
|
18
|
+
user: User;
|
|
19
|
+
profile: NormalizedSSOProfile;
|
|
20
|
+
provider: SSOProvider<SSOOptions>;
|
|
21
|
+
token?: OAuth2Tokens;
|
|
22
|
+
provisioningOptions?: OrganizationProvisioningOptions;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Assigns a user to an organization based on the SSO provider's organizationId.
|
|
27
|
+
* Used in SSO flows (OIDC, SAML) where the provider is already linked to an org.
|
|
28
|
+
*/
|
|
29
|
+
export async function assignOrganizationFromProvider(
|
|
30
|
+
ctx: GenericEndpointContext,
|
|
31
|
+
options: AssignOrganizationFromProviderOptions,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const { user, profile, provider, token, provisioningOptions } = options;
|
|
34
|
+
|
|
35
|
+
if (!provider.organizationId) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (provisioningOptions?.disabled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!ctx.context.hasPlugin("organization")) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isAlreadyMember = await ctx.context.adapter.findOne({
|
|
48
|
+
model: "member",
|
|
49
|
+
where: [
|
|
50
|
+
{ field: "organizationId", value: provider.organizationId },
|
|
51
|
+
{ field: "userId", value: user.id },
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (isAlreadyMember) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const role = provisioningOptions?.getRole
|
|
60
|
+
? await provisioningOptions.getRole({
|
|
61
|
+
user,
|
|
62
|
+
userInfo: profile.rawAttributes || {},
|
|
63
|
+
token,
|
|
64
|
+
provider,
|
|
65
|
+
})
|
|
66
|
+
: provisioningOptions?.defaultRole || "member";
|
|
67
|
+
|
|
68
|
+
await ctx.context.adapter.create({
|
|
69
|
+
model: "member",
|
|
70
|
+
data: {
|
|
71
|
+
organizationId: provider.organizationId,
|
|
72
|
+
userId: user.id,
|
|
73
|
+
role,
|
|
74
|
+
createdAt: new Date(),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface AssignOrganizationByDomainOptions {
|
|
80
|
+
user: User;
|
|
81
|
+
provisioningOptions?: OrganizationProvisioningOptions;
|
|
82
|
+
domainVerification?: {
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Assigns a user to an organization based on their email domain.
|
|
89
|
+
* Looks up SSO providers that match the user's email domain and assigns
|
|
90
|
+
* the user to the associated organization.
|
|
91
|
+
*
|
|
92
|
+
* This enables domain-based org assignment for non-SSO sign-in methods
|
|
93
|
+
* (e.g., Google OAuth with @acme.com email gets added to Acme's org).
|
|
94
|
+
*/
|
|
95
|
+
export async function assignOrganizationByDomain(
|
|
96
|
+
ctx: GenericEndpointContext,
|
|
97
|
+
options: AssignOrganizationByDomainOptions,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const { user, provisioningOptions, domainVerification } = options;
|
|
100
|
+
|
|
101
|
+
if (provisioningOptions?.disabled) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!ctx.context.hasPlugin("organization")) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const domain = user.email.split("@")[1];
|
|
110
|
+
if (!domain) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Support comma-separated domains for multi-domain SSO
|
|
115
|
+
// First try exact match (fast path)
|
|
116
|
+
const whereClause: { field: string; value: string | boolean }[] = [
|
|
117
|
+
{ field: "domain", value: domain },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
if (domainVerification?.enabled) {
|
|
121
|
+
whereClause.push({ field: "domainVerified", value: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let ssoProvider = await ctx.context.adapter.findOne<SSOProvider<SSOOptions>>({
|
|
125
|
+
model: "ssoProvider",
|
|
126
|
+
where: whereClause,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// If not found, search all providers for comma-separated domain match
|
|
130
|
+
if (!ssoProvider) {
|
|
131
|
+
const allProviders = await ctx.context.adapter.findMany<
|
|
132
|
+
SSOProvider<SSOOptions>
|
|
133
|
+
>({
|
|
134
|
+
model: "ssoProvider",
|
|
135
|
+
where: domainVerification?.enabled
|
|
136
|
+
? [{ field: "domainVerified", value: true }]
|
|
137
|
+
: [],
|
|
138
|
+
});
|
|
139
|
+
ssoProvider =
|
|
140
|
+
allProviders.find((p) => domainMatches(domain, p.domain)) ?? null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!ssoProvider || !ssoProvider.organizationId) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const isAlreadyMember = await ctx.context.adapter.findOne({
|
|
148
|
+
model: "member",
|
|
149
|
+
where: [
|
|
150
|
+
{ field: "organizationId", value: ssoProvider.organizationId },
|
|
151
|
+
{ field: "userId", value: user.id },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (isAlreadyMember) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const role = provisioningOptions?.getRole
|
|
160
|
+
? await provisioningOptions.getRole({
|
|
161
|
+
user,
|
|
162
|
+
userInfo: {},
|
|
163
|
+
provider: ssoProvider,
|
|
164
|
+
})
|
|
165
|
+
: provisioningOptions?.defaultRole || "member";
|
|
166
|
+
|
|
167
|
+
await ctx.context.adapter.create({
|
|
168
|
+
model: "member",
|
|
169
|
+
data: {
|
|
170
|
+
organizationId: ssoProvider.organizationId,
|
|
171
|
+
userId: user.id,
|
|
172
|
+
role,
|
|
173
|
+
createdAt: new Date(),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|