@better-auth/sso 1.3.27 → 1.4.0-beta.10
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 +23 -15
- package/dist/client.cjs +8 -6
- package/dist/client.d.cts +6 -8
- package/dist/client.d.ts +6 -8
- package/dist/client.js +12 -0
- package/dist/index-D0i1jHBp.d.cts +959 -0
- package/dist/index-Drgwy6ZL.d.ts +959 -0
- package/dist/index.cjs +2 -1671
- package/dist/index.d.cts +2 -959
- package/dist/index.d.ts +2 -959
- package/dist/index.js +3 -0
- package/dist/src-DLLHZrD9.cjs +1250 -0
- package/dist/src-DrA9mJEu.js +1212 -0
- package/package.json +11 -10
- package/tsdown.config.ts +8 -0
- package/build.config.ts +0 -12
- package/dist/client.d.mts +0 -11
- package/dist/client.mjs +0 -8
- package/dist/index.d.mts +0 -959
- package/dist/index.mjs +0 -1655
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let better_auth = require("better-auth");
|
|
25
|
+
better_auth = __toESM(better_auth);
|
|
26
|
+
let better_auth_api = require("better-auth/api");
|
|
27
|
+
better_auth_api = __toESM(better_auth_api);
|
|
28
|
+
let better_auth_oauth2 = require("better-auth/oauth2");
|
|
29
|
+
better_auth_oauth2 = __toESM(better_auth_oauth2);
|
|
30
|
+
let better_auth_plugins = require("better-auth/plugins");
|
|
31
|
+
better_auth_plugins = __toESM(better_auth_plugins);
|
|
32
|
+
let zod_v4 = require("zod/v4");
|
|
33
|
+
zod_v4 = __toESM(zod_v4);
|
|
34
|
+
let samlify = require("samlify");
|
|
35
|
+
samlify = __toESM(samlify);
|
|
36
|
+
let __better_fetch_fetch = require("@better-fetch/fetch");
|
|
37
|
+
__better_fetch_fetch = __toESM(__better_fetch_fetch);
|
|
38
|
+
let jose = require("jose");
|
|
39
|
+
jose = __toESM(jose);
|
|
40
|
+
let better_auth_cookies = require("better-auth/cookies");
|
|
41
|
+
better_auth_cookies = __toESM(better_auth_cookies);
|
|
42
|
+
let fast_xml_parser = require("fast-xml-parser");
|
|
43
|
+
fast_xml_parser = __toESM(fast_xml_parser);
|
|
44
|
+
|
|
45
|
+
//#region src/index.ts
|
|
46
|
+
samlify.setSchemaValidator({ async validate(xml) {
|
|
47
|
+
if (fast_xml_parser.XMLValidator.validate(xml, { allowBooleanAttributes: true }) === true) return "SUCCESS_VALIDATE_XML";
|
|
48
|
+
throw "ERR_INVALID_XML";
|
|
49
|
+
} });
|
|
50
|
+
/**
|
|
51
|
+
* Safely parses a value that might be a JSON string or already a parsed object
|
|
52
|
+
* This handles cases where ORMs like Drizzle might return already parsed objects
|
|
53
|
+
* instead of JSON strings from TEXT/JSON columns
|
|
54
|
+
*/
|
|
55
|
+
function safeJsonParse(value) {
|
|
56
|
+
if (!value) return null;
|
|
57
|
+
if (typeof value === "object") return value;
|
|
58
|
+
if (typeof value === "string") try {
|
|
59
|
+
return JSON.parse(value);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const sso = (options) => {
|
|
66
|
+
return {
|
|
67
|
+
id: "sso",
|
|
68
|
+
endpoints: {
|
|
69
|
+
spMetadata: (0, better_auth_plugins.createAuthEndpoint)("/sso/saml2/sp/metadata", {
|
|
70
|
+
method: "GET",
|
|
71
|
+
query: zod_v4.object({
|
|
72
|
+
providerId: zod_v4.string(),
|
|
73
|
+
format: zod_v4.enum(["xml", "json"]).default("xml")
|
|
74
|
+
}),
|
|
75
|
+
metadata: { openapi: {
|
|
76
|
+
summary: "Get Service Provider metadata",
|
|
77
|
+
description: "Returns the SAML metadata for the Service Provider",
|
|
78
|
+
responses: { "200": { description: "SAML metadata in XML format" } }
|
|
79
|
+
} }
|
|
80
|
+
}, async (ctx) => {
|
|
81
|
+
const provider = await ctx.context.adapter.findOne({
|
|
82
|
+
model: "ssoProvider",
|
|
83
|
+
where: [{
|
|
84
|
+
field: "providerId",
|
|
85
|
+
value: ctx.query.providerId
|
|
86
|
+
}]
|
|
87
|
+
});
|
|
88
|
+
if (!provider) throw new better_auth_api.APIError("NOT_FOUND", { message: "No provider found for the given providerId" });
|
|
89
|
+
const parsedSamlConfig = safeJsonParse(provider.samlConfig);
|
|
90
|
+
if (!parsedSamlConfig) throw new better_auth_api.APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
91
|
+
const sp = parsedSamlConfig.spMetadata.metadata ? samlify.ServiceProvider({ metadata: parsedSamlConfig.spMetadata.metadata }) : samlify.SPMetadata({
|
|
92
|
+
entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
|
|
93
|
+
assertionConsumerService: [{
|
|
94
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
95
|
+
Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${provider.id}`
|
|
96
|
+
}],
|
|
97
|
+
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
98
|
+
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
99
|
+
});
|
|
100
|
+
return new Response(sp.getMetadata(), { headers: { "Content-Type": "application/xml" } });
|
|
101
|
+
}),
|
|
102
|
+
registerSSOProvider: (0, better_auth_plugins.createAuthEndpoint)("/sso/register", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
body: zod_v4.object({
|
|
105
|
+
providerId: zod_v4.string({}).meta({ description: "The ID of the provider. This is used to identify the provider during login and callback" }),
|
|
106
|
+
issuer: zod_v4.string({}).meta({ description: "The issuer of the provider" }),
|
|
107
|
+
domain: zod_v4.string({}).meta({ description: "The domain of the provider. This is used for email matching" }),
|
|
108
|
+
oidcConfig: zod_v4.object({
|
|
109
|
+
clientId: zod_v4.string({}).meta({ description: "The client ID" }),
|
|
110
|
+
clientSecret: zod_v4.string({}).meta({ description: "The client secret" }),
|
|
111
|
+
authorizationEndpoint: zod_v4.string({}).meta({ description: "The authorization endpoint" }).optional(),
|
|
112
|
+
tokenEndpoint: zod_v4.string({}).meta({ description: "The token endpoint" }).optional(),
|
|
113
|
+
userInfoEndpoint: zod_v4.string({}).meta({ description: "The user info endpoint" }).optional(),
|
|
114
|
+
tokenEndpointAuthentication: zod_v4.enum(["client_secret_post", "client_secret_basic"]).optional(),
|
|
115
|
+
jwksEndpoint: zod_v4.string({}).meta({ description: "The JWKS endpoint" }).optional(),
|
|
116
|
+
discoveryEndpoint: zod_v4.string().optional(),
|
|
117
|
+
scopes: zod_v4.array(zod_v4.string(), {}).meta({ description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']" }).optional(),
|
|
118
|
+
pkce: zod_v4.boolean({}).meta({ description: "Whether to use PKCE for the authorization flow" }).default(true).optional(),
|
|
119
|
+
mapping: zod_v4.object({
|
|
120
|
+
id: zod_v4.string({}).meta({ description: "Field mapping for user ID (defaults to 'sub')" }),
|
|
121
|
+
email: zod_v4.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
|
|
122
|
+
emailVerified: zod_v4.string({}).meta({ description: "Field mapping for email verification (defaults to 'email_verified')" }).optional(),
|
|
123
|
+
name: zod_v4.string({}).meta({ description: "Field mapping for name (defaults to 'name')" }),
|
|
124
|
+
image: zod_v4.string({}).meta({ description: "Field mapping for image (defaults to 'picture')" }).optional(),
|
|
125
|
+
extraFields: zod_v4.record(zod_v4.string(), zod_v4.any()).optional()
|
|
126
|
+
}).optional()
|
|
127
|
+
}).optional(),
|
|
128
|
+
samlConfig: zod_v4.object({
|
|
129
|
+
entryPoint: zod_v4.string({}).meta({ description: "The entry point of the provider" }),
|
|
130
|
+
cert: zod_v4.string({}).meta({ description: "The certificate of the provider" }),
|
|
131
|
+
callbackUrl: zod_v4.string({}).meta({ description: "The callback URL of the provider" }),
|
|
132
|
+
audience: zod_v4.string().optional(),
|
|
133
|
+
idpMetadata: zod_v4.object({
|
|
134
|
+
metadata: zod_v4.string().optional(),
|
|
135
|
+
entityID: zod_v4.string().optional(),
|
|
136
|
+
cert: zod_v4.string().optional(),
|
|
137
|
+
privateKey: zod_v4.string().optional(),
|
|
138
|
+
privateKeyPass: zod_v4.string().optional(),
|
|
139
|
+
isAssertionEncrypted: zod_v4.boolean().optional(),
|
|
140
|
+
encPrivateKey: zod_v4.string().optional(),
|
|
141
|
+
encPrivateKeyPass: zod_v4.string().optional(),
|
|
142
|
+
singleSignOnService: zod_v4.array(zod_v4.object({
|
|
143
|
+
Binding: zod_v4.string().meta({ description: "The binding type for the SSO service" }),
|
|
144
|
+
Location: zod_v4.string().meta({ description: "The URL for the SSO service" })
|
|
145
|
+
})).optional().meta({ description: "Single Sign-On service configuration" })
|
|
146
|
+
}).optional(),
|
|
147
|
+
spMetadata: zod_v4.object({
|
|
148
|
+
metadata: zod_v4.string().optional(),
|
|
149
|
+
entityID: zod_v4.string().optional(),
|
|
150
|
+
binding: zod_v4.string().optional(),
|
|
151
|
+
privateKey: zod_v4.string().optional(),
|
|
152
|
+
privateKeyPass: zod_v4.string().optional(),
|
|
153
|
+
isAssertionEncrypted: zod_v4.boolean().optional(),
|
|
154
|
+
encPrivateKey: zod_v4.string().optional(),
|
|
155
|
+
encPrivateKeyPass: zod_v4.string().optional()
|
|
156
|
+
}),
|
|
157
|
+
wantAssertionsSigned: zod_v4.boolean().optional(),
|
|
158
|
+
signatureAlgorithm: zod_v4.string().optional(),
|
|
159
|
+
digestAlgorithm: zod_v4.string().optional(),
|
|
160
|
+
identifierFormat: zod_v4.string().optional(),
|
|
161
|
+
privateKey: zod_v4.string().optional(),
|
|
162
|
+
decryptionPvk: zod_v4.string().optional(),
|
|
163
|
+
additionalParams: zod_v4.record(zod_v4.string(), zod_v4.any()).optional(),
|
|
164
|
+
mapping: zod_v4.object({
|
|
165
|
+
id: zod_v4.string({}).meta({ description: "Field mapping for user ID (defaults to 'nameID')" }),
|
|
166
|
+
email: zod_v4.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
|
|
167
|
+
emailVerified: zod_v4.string({}).meta({ description: "Field mapping for email verification" }).optional(),
|
|
168
|
+
name: zod_v4.string({}).meta({ description: "Field mapping for name (defaults to 'displayName')" }),
|
|
169
|
+
firstName: zod_v4.string({}).meta({ description: "Field mapping for first name (defaults to 'givenName')" }).optional(),
|
|
170
|
+
lastName: zod_v4.string({}).meta({ description: "Field mapping for last name (defaults to 'surname')" }).optional(),
|
|
171
|
+
extraFields: zod_v4.record(zod_v4.string(), zod_v4.any()).optional()
|
|
172
|
+
}).optional()
|
|
173
|
+
}).optional(),
|
|
174
|
+
organizationId: zod_v4.string({}).meta({ description: "If organization plugin is enabled, the organization id to link the provider to" }).optional(),
|
|
175
|
+
overrideUserInfo: zod_v4.boolean({}).meta({ description: "Override user info with the provider info. Defaults to false" }).default(false).optional()
|
|
176
|
+
}),
|
|
177
|
+
use: [better_auth_api.sessionMiddleware],
|
|
178
|
+
metadata: { openapi: {
|
|
179
|
+
summary: "Register an OIDC provider",
|
|
180
|
+
description: "This endpoint is used to register an OIDC provider. This is used to configure the provider and link it to an organization",
|
|
181
|
+
responses: { "200": {
|
|
182
|
+
description: "OIDC provider created successfully",
|
|
183
|
+
content: { "application/json": { schema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
issuer: {
|
|
187
|
+
type: "string",
|
|
188
|
+
format: "uri",
|
|
189
|
+
description: "The issuer URL of the provider"
|
|
190
|
+
},
|
|
191
|
+
domain: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "The domain of the provider, used for email matching"
|
|
194
|
+
},
|
|
195
|
+
oidcConfig: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
issuer: {
|
|
199
|
+
type: "string",
|
|
200
|
+
format: "uri",
|
|
201
|
+
description: "The issuer URL of the provider"
|
|
202
|
+
},
|
|
203
|
+
pkce: {
|
|
204
|
+
type: "boolean",
|
|
205
|
+
description: "Whether PKCE is enabled for the authorization flow"
|
|
206
|
+
},
|
|
207
|
+
clientId: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "The client ID for the provider"
|
|
210
|
+
},
|
|
211
|
+
clientSecret: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "The client secret for the provider"
|
|
214
|
+
},
|
|
215
|
+
authorizationEndpoint: {
|
|
216
|
+
type: "string",
|
|
217
|
+
format: "uri",
|
|
218
|
+
nullable: true,
|
|
219
|
+
description: "The authorization endpoint URL"
|
|
220
|
+
},
|
|
221
|
+
discoveryEndpoint: {
|
|
222
|
+
type: "string",
|
|
223
|
+
format: "uri",
|
|
224
|
+
description: "The discovery endpoint URL"
|
|
225
|
+
},
|
|
226
|
+
userInfoEndpoint: {
|
|
227
|
+
type: "string",
|
|
228
|
+
format: "uri",
|
|
229
|
+
nullable: true,
|
|
230
|
+
description: "The user info endpoint URL"
|
|
231
|
+
},
|
|
232
|
+
scopes: {
|
|
233
|
+
type: "array",
|
|
234
|
+
items: { type: "string" },
|
|
235
|
+
nullable: true,
|
|
236
|
+
description: "The scopes requested from the provider"
|
|
237
|
+
},
|
|
238
|
+
tokenEndpoint: {
|
|
239
|
+
type: "string",
|
|
240
|
+
format: "uri",
|
|
241
|
+
nullable: true,
|
|
242
|
+
description: "The token endpoint URL"
|
|
243
|
+
},
|
|
244
|
+
tokenEndpointAuthentication: {
|
|
245
|
+
type: "string",
|
|
246
|
+
enum: ["client_secret_post", "client_secret_basic"],
|
|
247
|
+
nullable: true,
|
|
248
|
+
description: "Authentication method for the token endpoint"
|
|
249
|
+
},
|
|
250
|
+
jwksEndpoint: {
|
|
251
|
+
type: "string",
|
|
252
|
+
format: "uri",
|
|
253
|
+
nullable: true,
|
|
254
|
+
description: "The JWKS endpoint URL"
|
|
255
|
+
},
|
|
256
|
+
mapping: {
|
|
257
|
+
type: "object",
|
|
258
|
+
nullable: true,
|
|
259
|
+
properties: {
|
|
260
|
+
id: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "Field mapping for user ID (defaults to 'sub')"
|
|
263
|
+
},
|
|
264
|
+
email: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Field mapping for email (defaults to 'email')"
|
|
267
|
+
},
|
|
268
|
+
emailVerified: {
|
|
269
|
+
type: "string",
|
|
270
|
+
nullable: true,
|
|
271
|
+
description: "Field mapping for email verification (defaults to 'email_verified')"
|
|
272
|
+
},
|
|
273
|
+
name: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "Field mapping for name (defaults to 'name')"
|
|
276
|
+
},
|
|
277
|
+
image: {
|
|
278
|
+
type: "string",
|
|
279
|
+
nullable: true,
|
|
280
|
+
description: "Field mapping for image (defaults to 'picture')"
|
|
281
|
+
},
|
|
282
|
+
extraFields: {
|
|
283
|
+
type: "object",
|
|
284
|
+
additionalProperties: { type: "string" },
|
|
285
|
+
nullable: true,
|
|
286
|
+
description: "Additional field mappings"
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
required: [
|
|
290
|
+
"id",
|
|
291
|
+
"email",
|
|
292
|
+
"name"
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
required: [
|
|
297
|
+
"issuer",
|
|
298
|
+
"pkce",
|
|
299
|
+
"clientId",
|
|
300
|
+
"clientSecret",
|
|
301
|
+
"discoveryEndpoint"
|
|
302
|
+
],
|
|
303
|
+
description: "OIDC configuration for the provider"
|
|
304
|
+
},
|
|
305
|
+
organizationId: {
|
|
306
|
+
type: "string",
|
|
307
|
+
nullable: true,
|
|
308
|
+
description: "ID of the linked organization, if any"
|
|
309
|
+
},
|
|
310
|
+
userId: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "ID of the user who registered the provider"
|
|
313
|
+
},
|
|
314
|
+
providerId: {
|
|
315
|
+
type: "string",
|
|
316
|
+
description: "Unique identifier for the provider"
|
|
317
|
+
},
|
|
318
|
+
redirectURI: {
|
|
319
|
+
type: "string",
|
|
320
|
+
format: "uri",
|
|
321
|
+
description: "The redirect URI for the provider callback"
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
required: [
|
|
325
|
+
"issuer",
|
|
326
|
+
"domain",
|
|
327
|
+
"oidcConfig",
|
|
328
|
+
"userId",
|
|
329
|
+
"providerId",
|
|
330
|
+
"redirectURI"
|
|
331
|
+
]
|
|
332
|
+
} } }
|
|
333
|
+
} }
|
|
334
|
+
} }
|
|
335
|
+
}, async (ctx) => {
|
|
336
|
+
const user = ctx.context.session?.user;
|
|
337
|
+
if (!user) throw new better_auth_api.APIError("UNAUTHORIZED");
|
|
338
|
+
const limit = typeof options?.providersLimit === "function" ? await options.providersLimit(user) : options?.providersLimit ?? 10;
|
|
339
|
+
if (!limit) throw new better_auth_api.APIError("FORBIDDEN", { message: "SSO provider registration is disabled" });
|
|
340
|
+
if ((await ctx.context.adapter.findMany({
|
|
341
|
+
model: "ssoProvider",
|
|
342
|
+
where: [{
|
|
343
|
+
field: "userId",
|
|
344
|
+
value: user.id
|
|
345
|
+
}]
|
|
346
|
+
})).length >= limit) throw new better_auth_api.APIError("FORBIDDEN", { message: "You have reached the maximum number of SSO providers" });
|
|
347
|
+
const body = ctx.body;
|
|
348
|
+
if (zod_v4.string().url().safeParse(body.issuer).error) throw new better_auth_api.APIError("BAD_REQUEST", { message: "Invalid issuer. Must be a valid URL" });
|
|
349
|
+
if (ctx.body.organizationId) {
|
|
350
|
+
if (!await ctx.context.adapter.findOne({
|
|
351
|
+
model: "member",
|
|
352
|
+
where: [{
|
|
353
|
+
field: "userId",
|
|
354
|
+
value: user.id
|
|
355
|
+
}, {
|
|
356
|
+
field: "organizationId",
|
|
357
|
+
value: ctx.body.organizationId
|
|
358
|
+
}]
|
|
359
|
+
})) throw new better_auth_api.APIError("BAD_REQUEST", { message: "You are not a member of the organization" });
|
|
360
|
+
}
|
|
361
|
+
if (await ctx.context.adapter.findOne({
|
|
362
|
+
model: "ssoProvider",
|
|
363
|
+
where: [{
|
|
364
|
+
field: "providerId",
|
|
365
|
+
value: body.providerId
|
|
366
|
+
}]
|
|
367
|
+
})) {
|
|
368
|
+
ctx.context.logger.info(`SSO provider creation attempt with existing providerId: ${body.providerId}`);
|
|
369
|
+
throw new better_auth_api.APIError("UNPROCESSABLE_ENTITY", { message: "SSO provider with this providerId already exists" });
|
|
370
|
+
}
|
|
371
|
+
const provider = await ctx.context.adapter.create({
|
|
372
|
+
model: "ssoProvider",
|
|
373
|
+
data: {
|
|
374
|
+
issuer: body.issuer,
|
|
375
|
+
domain: body.domain,
|
|
376
|
+
oidcConfig: body.oidcConfig ? JSON.stringify({
|
|
377
|
+
issuer: body.issuer,
|
|
378
|
+
clientId: body.oidcConfig.clientId,
|
|
379
|
+
clientSecret: body.oidcConfig.clientSecret,
|
|
380
|
+
authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
|
|
381
|
+
tokenEndpoint: body.oidcConfig.tokenEndpoint,
|
|
382
|
+
tokenEndpointAuthentication: body.oidcConfig.tokenEndpointAuthentication,
|
|
383
|
+
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
384
|
+
pkce: body.oidcConfig.pkce,
|
|
385
|
+
discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
|
|
386
|
+
mapping: body.oidcConfig.mapping,
|
|
387
|
+
scopes: body.oidcConfig.scopes,
|
|
388
|
+
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
389
|
+
overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
|
|
390
|
+
}) : null,
|
|
391
|
+
samlConfig: body.samlConfig ? JSON.stringify({
|
|
392
|
+
issuer: body.issuer,
|
|
393
|
+
entryPoint: body.samlConfig.entryPoint,
|
|
394
|
+
cert: body.samlConfig.cert,
|
|
395
|
+
callbackUrl: body.samlConfig.callbackUrl,
|
|
396
|
+
audience: body.samlConfig.audience,
|
|
397
|
+
idpMetadata: body.samlConfig.idpMetadata,
|
|
398
|
+
spMetadata: body.samlConfig.spMetadata,
|
|
399
|
+
wantAssertionsSigned: body.samlConfig.wantAssertionsSigned,
|
|
400
|
+
signatureAlgorithm: body.samlConfig.signatureAlgorithm,
|
|
401
|
+
digestAlgorithm: body.samlConfig.digestAlgorithm,
|
|
402
|
+
identifierFormat: body.samlConfig.identifierFormat,
|
|
403
|
+
privateKey: body.samlConfig.privateKey,
|
|
404
|
+
decryptionPvk: body.samlConfig.decryptionPvk,
|
|
405
|
+
additionalParams: body.samlConfig.additionalParams,
|
|
406
|
+
mapping: body.samlConfig.mapping
|
|
407
|
+
}) : null,
|
|
408
|
+
organizationId: body.organizationId,
|
|
409
|
+
userId: ctx.context.session.user.id,
|
|
410
|
+
providerId: body.providerId
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
return ctx.json({
|
|
414
|
+
...provider,
|
|
415
|
+
oidcConfig: JSON.parse(provider.oidcConfig),
|
|
416
|
+
samlConfig: JSON.parse(provider.samlConfig),
|
|
417
|
+
redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}`
|
|
418
|
+
});
|
|
419
|
+
}),
|
|
420
|
+
signInSSO: (0, better_auth_plugins.createAuthEndpoint)("/sign-in/sso", {
|
|
421
|
+
method: "POST",
|
|
422
|
+
body: zod_v4.object({
|
|
423
|
+
email: zod_v4.string({}).meta({ description: "The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided" }).optional(),
|
|
424
|
+
organizationSlug: zod_v4.string({}).meta({ description: "The slug of the organization to sign in with" }).optional(),
|
|
425
|
+
providerId: zod_v4.string({}).meta({ description: "The ID of the provider to sign in with. This can be provided instead of email or issuer" }).optional(),
|
|
426
|
+
domain: zod_v4.string({}).meta({ description: "The domain of the provider." }).optional(),
|
|
427
|
+
callbackURL: zod_v4.string({}).meta({ description: "The URL to redirect to after login" }),
|
|
428
|
+
errorCallbackURL: zod_v4.string({}).meta({ description: "The URL to redirect to after login" }).optional(),
|
|
429
|
+
newUserCallbackURL: zod_v4.string({}).meta({ description: "The URL to redirect to after login if the user is new" }).optional(),
|
|
430
|
+
scopes: zod_v4.array(zod_v4.string(), {}).meta({ description: "Scopes to request from the provider." }).optional(),
|
|
431
|
+
requestSignUp: zod_v4.boolean({}).meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider" }).optional(),
|
|
432
|
+
providerType: zod_v4.enum(["oidc", "saml"]).optional()
|
|
433
|
+
}),
|
|
434
|
+
metadata: { openapi: {
|
|
435
|
+
summary: "Sign in with SSO provider",
|
|
436
|
+
description: "This endpoint is used to sign in with an SSO provider. It redirects to the provider's authorization URL",
|
|
437
|
+
requestBody: { content: { "application/json": { schema: {
|
|
438
|
+
type: "object",
|
|
439
|
+
properties: {
|
|
440
|
+
email: {
|
|
441
|
+
type: "string",
|
|
442
|
+
description: "The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided"
|
|
443
|
+
},
|
|
444
|
+
issuer: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "The issuer identifier, this is the URL of the provider and can be used to verify the provider and identify the provider during login. It's optional if the email is provided"
|
|
447
|
+
},
|
|
448
|
+
providerId: {
|
|
449
|
+
type: "string",
|
|
450
|
+
description: "The ID of the provider to sign in with. This can be provided instead of email or issuer"
|
|
451
|
+
},
|
|
452
|
+
callbackURL: {
|
|
453
|
+
type: "string",
|
|
454
|
+
description: "The URL to redirect to after login"
|
|
455
|
+
},
|
|
456
|
+
errorCallbackURL: {
|
|
457
|
+
type: "string",
|
|
458
|
+
description: "The URL to redirect to after login"
|
|
459
|
+
},
|
|
460
|
+
newUserCallbackURL: {
|
|
461
|
+
type: "string",
|
|
462
|
+
description: "The URL to redirect to after login if the user is new"
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
required: ["callbackURL"]
|
|
466
|
+
} } } },
|
|
467
|
+
responses: { "200": {
|
|
468
|
+
description: "Authorization URL generated successfully for SSO sign-in",
|
|
469
|
+
content: { "application/json": { schema: {
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
url: {
|
|
473
|
+
type: "string",
|
|
474
|
+
format: "uri",
|
|
475
|
+
description: "The authorization URL to redirect the user to for SSO sign-in"
|
|
476
|
+
},
|
|
477
|
+
redirect: {
|
|
478
|
+
type: "boolean",
|
|
479
|
+
description: "Indicates that the client should redirect to the provided URL",
|
|
480
|
+
enum: [true]
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
required: ["url", "redirect"]
|
|
484
|
+
} } }
|
|
485
|
+
} }
|
|
486
|
+
} }
|
|
487
|
+
}, async (ctx) => {
|
|
488
|
+
const body = ctx.body;
|
|
489
|
+
let { email, organizationSlug, providerId, domain } = body;
|
|
490
|
+
if (!options?.defaultSSO?.length && !email && !organizationSlug && !domain && !providerId) throw new better_auth_api.APIError("BAD_REQUEST", { message: "email, organizationSlug, domain or providerId is required" });
|
|
491
|
+
domain = body.domain || email?.split("@")[1];
|
|
492
|
+
let orgId = "";
|
|
493
|
+
if (organizationSlug) orgId = await ctx.context.adapter.findOne({
|
|
494
|
+
model: "organization",
|
|
495
|
+
where: [{
|
|
496
|
+
field: "slug",
|
|
497
|
+
value: organizationSlug
|
|
498
|
+
}]
|
|
499
|
+
}).then((res) => {
|
|
500
|
+
if (!res) return "";
|
|
501
|
+
return res.id;
|
|
502
|
+
});
|
|
503
|
+
let provider = null;
|
|
504
|
+
if (options?.defaultSSO?.length) {
|
|
505
|
+
const matchingDefault = providerId ? options.defaultSSO.find((defaultProvider) => defaultProvider.providerId === providerId) : options.defaultSSO.find((defaultProvider) => defaultProvider.domain === domain);
|
|
506
|
+
if (matchingDefault) provider = {
|
|
507
|
+
issuer: matchingDefault.samlConfig?.issuer || matchingDefault.oidcConfig?.issuer || "",
|
|
508
|
+
providerId: matchingDefault.providerId,
|
|
509
|
+
userId: "default",
|
|
510
|
+
oidcConfig: matchingDefault.oidcConfig,
|
|
511
|
+
samlConfig: matchingDefault.samlConfig
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
if (!providerId && !orgId && !domain) throw new better_auth_api.APIError("BAD_REQUEST", { message: "providerId, orgId or domain is required" });
|
|
515
|
+
if (!provider) provider = await ctx.context.adapter.findOne({
|
|
516
|
+
model: "ssoProvider",
|
|
517
|
+
where: [{
|
|
518
|
+
field: providerId ? "providerId" : orgId ? "organizationId" : "domain",
|
|
519
|
+
value: providerId || orgId || domain
|
|
520
|
+
}]
|
|
521
|
+
}).then((res) => {
|
|
522
|
+
if (!res) return null;
|
|
523
|
+
return {
|
|
524
|
+
...res,
|
|
525
|
+
oidcConfig: res.oidcConfig ? safeJsonParse(res.oidcConfig) || void 0 : void 0,
|
|
526
|
+
samlConfig: res.samlConfig ? safeJsonParse(res.samlConfig) || void 0 : void 0
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
if (!provider) throw new better_auth_api.APIError("NOT_FOUND", { message: "No provider found for the issuer" });
|
|
530
|
+
if (body.providerType) {
|
|
531
|
+
if (body.providerType === "oidc" && !provider.oidcConfig) throw new better_auth_api.APIError("BAD_REQUEST", { message: "OIDC provider is not configured" });
|
|
532
|
+
if (body.providerType === "saml" && !provider.samlConfig) throw new better_auth_api.APIError("BAD_REQUEST", { message: "SAML provider is not configured" });
|
|
533
|
+
}
|
|
534
|
+
if (provider.oidcConfig && body.providerType !== "saml") {
|
|
535
|
+
const state = await (0, better_auth.generateState)(ctx);
|
|
536
|
+
const redirectURI = `${ctx.context.baseURL}/sso/callback/${provider.providerId}`;
|
|
537
|
+
const authorizationURL = await (0, better_auth_oauth2.createAuthorizationURL)({
|
|
538
|
+
id: provider.issuer,
|
|
539
|
+
options: {
|
|
540
|
+
clientId: provider.oidcConfig.clientId,
|
|
541
|
+
clientSecret: provider.oidcConfig.clientSecret
|
|
542
|
+
},
|
|
543
|
+
redirectURI,
|
|
544
|
+
state: state.state,
|
|
545
|
+
codeVerifier: provider.oidcConfig.pkce ? state.codeVerifier : void 0,
|
|
546
|
+
scopes: ctx.body.scopes || provider.oidcConfig.scopes || [
|
|
547
|
+
"openid",
|
|
548
|
+
"email",
|
|
549
|
+
"profile",
|
|
550
|
+
"offline_access"
|
|
551
|
+
],
|
|
552
|
+
authorizationEndpoint: provider.oidcConfig.authorizationEndpoint
|
|
553
|
+
});
|
|
554
|
+
return ctx.json({
|
|
555
|
+
url: authorizationURL.toString(),
|
|
556
|
+
redirect: true
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (provider.samlConfig) {
|
|
560
|
+
const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : safeJsonParse(provider.samlConfig);
|
|
561
|
+
if (!parsedSamlConfig) throw new better_auth_api.APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
562
|
+
const sp = samlify.ServiceProvider({
|
|
563
|
+
metadata: parsedSamlConfig.spMetadata.metadata,
|
|
564
|
+
allowCreate: true
|
|
565
|
+
});
|
|
566
|
+
const idp = samlify.IdentityProvider({
|
|
567
|
+
metadata: parsedSamlConfig.idpMetadata?.metadata,
|
|
568
|
+
entityID: parsedSamlConfig.idpMetadata?.entityID,
|
|
569
|
+
encryptCert: parsedSamlConfig.idpMetadata?.cert,
|
|
570
|
+
singleSignOnService: parsedSamlConfig.idpMetadata?.singleSignOnService
|
|
571
|
+
});
|
|
572
|
+
const loginRequest = sp.createLoginRequest(idp, "redirect");
|
|
573
|
+
if (!loginRequest) throw new better_auth_api.APIError("BAD_REQUEST", { message: "Invalid SAML request" });
|
|
574
|
+
return ctx.json({
|
|
575
|
+
url: `${loginRequest.context}&RelayState=${encodeURIComponent(body.callbackURL)}`,
|
|
576
|
+
redirect: true
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
throw new better_auth_api.APIError("BAD_REQUEST", { message: "Invalid SSO provider" });
|
|
580
|
+
}),
|
|
581
|
+
callbackSSO: (0, better_auth_plugins.createAuthEndpoint)("/sso/callback/:providerId", {
|
|
582
|
+
method: "GET",
|
|
583
|
+
query: zod_v4.object({
|
|
584
|
+
code: zod_v4.string().optional(),
|
|
585
|
+
state: zod_v4.string(),
|
|
586
|
+
error: zod_v4.string().optional(),
|
|
587
|
+
error_description: zod_v4.string().optional()
|
|
588
|
+
}),
|
|
589
|
+
metadata: {
|
|
590
|
+
isAction: false,
|
|
591
|
+
openapi: {
|
|
592
|
+
summary: "Callback URL for SSO provider",
|
|
593
|
+
description: "This endpoint is used as the callback URL for SSO providers. It handles the authorization code and exchanges it for an access token",
|
|
594
|
+
responses: { "302": { description: "Redirects to the callback URL" } }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}, async (ctx) => {
|
|
598
|
+
const { code, state, error, error_description } = ctx.query;
|
|
599
|
+
const stateData = await (0, better_auth_oauth2.parseState)(ctx);
|
|
600
|
+
if (!stateData) {
|
|
601
|
+
const errorURL$1 = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
602
|
+
throw ctx.redirect(`${errorURL$1}?error=invalid_state`);
|
|
603
|
+
}
|
|
604
|
+
const { callbackURL, errorURL, newUserURL, requestSignUp } = stateData;
|
|
605
|
+
if (!code || error) throw ctx.redirect(`${errorURL || callbackURL}?error=${error}&error_description=${error_description}`);
|
|
606
|
+
let provider = null;
|
|
607
|
+
if (options?.defaultSSO?.length) {
|
|
608
|
+
const matchingDefault = options.defaultSSO.find((defaultProvider) => defaultProvider.providerId === ctx.params.providerId);
|
|
609
|
+
if (matchingDefault) provider = {
|
|
610
|
+
...matchingDefault,
|
|
611
|
+
issuer: matchingDefault.oidcConfig?.issuer || "",
|
|
612
|
+
userId: "default"
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
if (!provider) provider = await ctx.context.adapter.findOne({
|
|
616
|
+
model: "ssoProvider",
|
|
617
|
+
where: [{
|
|
618
|
+
field: "providerId",
|
|
619
|
+
value: ctx.params.providerId
|
|
620
|
+
}]
|
|
621
|
+
}).then((res) => {
|
|
622
|
+
if (!res) return null;
|
|
623
|
+
return {
|
|
624
|
+
...res,
|
|
625
|
+
oidcConfig: safeJsonParse(res.oidcConfig) || void 0
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
if (!provider) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`);
|
|
629
|
+
let config = provider.oidcConfig;
|
|
630
|
+
if (!config) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`);
|
|
631
|
+
const discovery = await (0, __better_fetch_fetch.betterFetch)(config.discoveryEndpoint);
|
|
632
|
+
if (discovery.data) config = {
|
|
633
|
+
tokenEndpoint: discovery.data.token_endpoint,
|
|
634
|
+
tokenEndpointAuthentication: discovery.data.token_endpoint_auth_method,
|
|
635
|
+
userInfoEndpoint: discovery.data.userinfo_endpoint,
|
|
636
|
+
scopes: [
|
|
637
|
+
"openid",
|
|
638
|
+
"email",
|
|
639
|
+
"profile",
|
|
640
|
+
"offline_access"
|
|
641
|
+
],
|
|
642
|
+
...config
|
|
643
|
+
};
|
|
644
|
+
if (!config.tokenEndpoint) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=token_endpoint_not_found`);
|
|
645
|
+
const tokenResponse = await (0, better_auth_oauth2.validateAuthorizationCode)({
|
|
646
|
+
code,
|
|
647
|
+
codeVerifier: config.pkce ? stateData.codeVerifier : void 0,
|
|
648
|
+
redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}`,
|
|
649
|
+
options: {
|
|
650
|
+
clientId: config.clientId,
|
|
651
|
+
clientSecret: config.clientSecret
|
|
652
|
+
},
|
|
653
|
+
tokenEndpoint: config.tokenEndpoint,
|
|
654
|
+
authentication: config.tokenEndpointAuthentication === "client_secret_post" ? "post" : "basic"
|
|
655
|
+
}).catch((e) => {
|
|
656
|
+
if (e instanceof __better_fetch_fetch.BetterFetchError) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=${e.message}`);
|
|
657
|
+
return null;
|
|
658
|
+
});
|
|
659
|
+
if (!tokenResponse) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=token_response_not_found`);
|
|
660
|
+
let userInfo = null;
|
|
661
|
+
if (tokenResponse.idToken) {
|
|
662
|
+
const idToken = (0, jose.decodeJwt)(tokenResponse.idToken);
|
|
663
|
+
if (!config.jwksEndpoint) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=jwks_endpoint_not_found`);
|
|
664
|
+
const verified = await (0, better_auth_oauth2.validateToken)(tokenResponse.idToken, config.jwksEndpoint).catch((e) => {
|
|
665
|
+
ctx.context.logger.error(e);
|
|
666
|
+
return null;
|
|
667
|
+
});
|
|
668
|
+
if (!verified) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=token_not_verified`);
|
|
669
|
+
if (verified.payload.iss !== provider.issuer) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=issuer_mismatch`);
|
|
670
|
+
const mapping = config.mapping || {};
|
|
671
|
+
userInfo = {
|
|
672
|
+
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, verified.payload[value]])),
|
|
673
|
+
id: idToken[mapping.id || "sub"],
|
|
674
|
+
email: idToken[mapping.email || "email"],
|
|
675
|
+
emailVerified: options?.trustEmailVerified ? idToken[mapping.emailVerified || "email_verified"] : false,
|
|
676
|
+
name: idToken[mapping.name || "name"],
|
|
677
|
+
image: idToken[mapping.image || "picture"]
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (!userInfo) {
|
|
681
|
+
if (!config.userInfoEndpoint) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=user_info_endpoint_not_found`);
|
|
682
|
+
const userInfoResponse = await (0, __better_fetch_fetch.betterFetch)(config.userInfoEndpoint, { headers: { Authorization: `Bearer ${tokenResponse.accessToken}` } });
|
|
683
|
+
if (userInfoResponse.error) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=${userInfoResponse.error.message}`);
|
|
684
|
+
userInfo = userInfoResponse.data;
|
|
685
|
+
}
|
|
686
|
+
if (!userInfo.email || !userInfo.id) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=missing_user_info`);
|
|
687
|
+
const linked = await (0, better_auth_oauth2.handleOAuthUserInfo)(ctx, {
|
|
688
|
+
userInfo: {
|
|
689
|
+
email: userInfo.email,
|
|
690
|
+
name: userInfo.name || userInfo.email,
|
|
691
|
+
id: userInfo.id,
|
|
692
|
+
image: userInfo.image,
|
|
693
|
+
emailVerified: options?.trustEmailVerified ? userInfo.emailVerified || false : false
|
|
694
|
+
},
|
|
695
|
+
account: {
|
|
696
|
+
idToken: tokenResponse.idToken,
|
|
697
|
+
accessToken: tokenResponse.accessToken,
|
|
698
|
+
refreshToken: tokenResponse.refreshToken,
|
|
699
|
+
accountId: userInfo.id,
|
|
700
|
+
providerId: provider.providerId,
|
|
701
|
+
accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
|
|
702
|
+
refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
|
|
703
|
+
scope: tokenResponse.scopes?.join(",")
|
|
704
|
+
},
|
|
705
|
+
callbackURL,
|
|
706
|
+
disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
|
|
707
|
+
overrideUserInfo: config.overrideUserInfo
|
|
708
|
+
});
|
|
709
|
+
if (linked.error) throw ctx.redirect(`${errorURL || callbackURL}/error?error=${linked.error}`);
|
|
710
|
+
const { session, user } = linked.data;
|
|
711
|
+
if (options?.provisionUser) await options.provisionUser({
|
|
712
|
+
user,
|
|
713
|
+
userInfo,
|
|
714
|
+
token: tokenResponse,
|
|
715
|
+
provider
|
|
716
|
+
});
|
|
717
|
+
if (provider.organizationId && !options?.organizationProvisioning?.disabled) {
|
|
718
|
+
if (ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) {
|
|
719
|
+
if (!await ctx.context.adapter.findOne({
|
|
720
|
+
model: "member",
|
|
721
|
+
where: [{
|
|
722
|
+
field: "organizationId",
|
|
723
|
+
value: provider.organizationId
|
|
724
|
+
}, {
|
|
725
|
+
field: "userId",
|
|
726
|
+
value: user.id
|
|
727
|
+
}]
|
|
728
|
+
})) {
|
|
729
|
+
const role = options?.organizationProvisioning?.getRole ? await options.organizationProvisioning.getRole({
|
|
730
|
+
user,
|
|
731
|
+
userInfo,
|
|
732
|
+
token: tokenResponse,
|
|
733
|
+
provider
|
|
734
|
+
}) : options?.organizationProvisioning?.defaultRole || "member";
|
|
735
|
+
await ctx.context.adapter.create({
|
|
736
|
+
model: "member",
|
|
737
|
+
data: {
|
|
738
|
+
organizationId: provider.organizationId,
|
|
739
|
+
userId: user.id,
|
|
740
|
+
role,
|
|
741
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
742
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
await (0, better_auth_cookies.setSessionCookie)(ctx, {
|
|
749
|
+
session,
|
|
750
|
+
user
|
|
751
|
+
});
|
|
752
|
+
let toRedirectTo;
|
|
753
|
+
try {
|
|
754
|
+
toRedirectTo = (linked.isRegister ? newUserURL || callbackURL : callbackURL).toString();
|
|
755
|
+
} catch {
|
|
756
|
+
toRedirectTo = linked.isRegister ? newUserURL || callbackURL : callbackURL;
|
|
757
|
+
}
|
|
758
|
+
throw ctx.redirect(toRedirectTo);
|
|
759
|
+
}),
|
|
760
|
+
callbackSSOSAML: (0, better_auth_plugins.createAuthEndpoint)("/sso/saml2/callback/:providerId", {
|
|
761
|
+
method: "POST",
|
|
762
|
+
body: zod_v4.object({
|
|
763
|
+
SAMLResponse: zod_v4.string(),
|
|
764
|
+
RelayState: zod_v4.string().optional()
|
|
765
|
+
}),
|
|
766
|
+
metadata: {
|
|
767
|
+
isAction: false,
|
|
768
|
+
openapi: {
|
|
769
|
+
summary: "Callback URL for SAML provider",
|
|
770
|
+
description: "This endpoint is used as the callback URL for SAML providers.",
|
|
771
|
+
responses: {
|
|
772
|
+
"302": { description: "Redirects to the callback URL" },
|
|
773
|
+
"400": { description: "Invalid SAML response" },
|
|
774
|
+
"401": { description: "Unauthorized - SAML authentication failed" }
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}, async (ctx) => {
|
|
779
|
+
const { SAMLResponse, RelayState } = ctx.body;
|
|
780
|
+
const { providerId } = ctx.params;
|
|
781
|
+
let provider = null;
|
|
782
|
+
if (options?.defaultSSO?.length) {
|
|
783
|
+
const matchingDefault = options.defaultSSO.find((defaultProvider) => defaultProvider.providerId === providerId);
|
|
784
|
+
if (matchingDefault) provider = {
|
|
785
|
+
...matchingDefault,
|
|
786
|
+
userId: "default",
|
|
787
|
+
issuer: matchingDefault.samlConfig?.issuer || ""
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
if (!provider) provider = await ctx.context.adapter.findOne({
|
|
791
|
+
model: "ssoProvider",
|
|
792
|
+
where: [{
|
|
793
|
+
field: "providerId",
|
|
794
|
+
value: providerId
|
|
795
|
+
}]
|
|
796
|
+
}).then((res) => {
|
|
797
|
+
if (!res) return null;
|
|
798
|
+
return {
|
|
799
|
+
...res,
|
|
800
|
+
samlConfig: res.samlConfig ? safeJsonParse(res.samlConfig) || void 0 : void 0
|
|
801
|
+
};
|
|
802
|
+
});
|
|
803
|
+
if (!provider) throw new better_auth_api.APIError("NOT_FOUND", { message: "No provider found for the given providerId" });
|
|
804
|
+
const parsedSamlConfig = safeJsonParse(provider.samlConfig);
|
|
805
|
+
if (!parsedSamlConfig) throw new better_auth_api.APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
806
|
+
const idpData = parsedSamlConfig.idpMetadata;
|
|
807
|
+
let idp = null;
|
|
808
|
+
if (!idpData?.metadata) idp = samlify.IdentityProvider({
|
|
809
|
+
entityID: idpData?.entityID || parsedSamlConfig.issuer,
|
|
810
|
+
singleSignOnService: [{
|
|
811
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
812
|
+
Location: parsedSamlConfig.entryPoint
|
|
813
|
+
}],
|
|
814
|
+
signingCert: idpData?.cert || parsedSamlConfig.cert,
|
|
815
|
+
wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
816
|
+
isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
|
|
817
|
+
encPrivateKey: idpData?.encPrivateKey,
|
|
818
|
+
encPrivateKeyPass: idpData?.encPrivateKeyPass
|
|
819
|
+
});
|
|
820
|
+
else idp = samlify.IdentityProvider({
|
|
821
|
+
metadata: idpData.metadata,
|
|
822
|
+
privateKey: idpData.privateKey,
|
|
823
|
+
privateKeyPass: idpData.privateKeyPass,
|
|
824
|
+
isAssertionEncrypted: idpData.isAssertionEncrypted,
|
|
825
|
+
encPrivateKey: idpData.encPrivateKey,
|
|
826
|
+
encPrivateKeyPass: idpData.encPrivateKeyPass
|
|
827
|
+
});
|
|
828
|
+
const spData = parsedSamlConfig.spMetadata;
|
|
829
|
+
const sp = samlify.ServiceProvider({
|
|
830
|
+
metadata: spData?.metadata,
|
|
831
|
+
entityID: spData?.entityID || parsedSamlConfig.issuer,
|
|
832
|
+
assertionConsumerService: spData?.metadata ? void 0 : [{
|
|
833
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
834
|
+
Location: parsedSamlConfig.callbackUrl
|
|
835
|
+
}],
|
|
836
|
+
privateKey: spData?.privateKey || parsedSamlConfig.privateKey,
|
|
837
|
+
privateKeyPass: spData?.privateKeyPass,
|
|
838
|
+
isAssertionEncrypted: spData?.isAssertionEncrypted || false,
|
|
839
|
+
encPrivateKey: spData?.encPrivateKey,
|
|
840
|
+
encPrivateKeyPass: spData?.encPrivateKeyPass,
|
|
841
|
+
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
842
|
+
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
843
|
+
});
|
|
844
|
+
let parsedResponse;
|
|
845
|
+
try {
|
|
846
|
+
const decodedResponse = Buffer.from(SAMLResponse, "base64").toString("utf-8");
|
|
847
|
+
try {
|
|
848
|
+
parsedResponse = await sp.parseLoginResponse(idp, "post", { body: {
|
|
849
|
+
SAMLResponse,
|
|
850
|
+
RelayState: RelayState || void 0
|
|
851
|
+
} });
|
|
852
|
+
} catch (parseError) {
|
|
853
|
+
const nameIDMatch = decodedResponse.match(/<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/);
|
|
854
|
+
if (!nameIDMatch) throw parseError;
|
|
855
|
+
parsedResponse = { extract: {
|
|
856
|
+
nameID: nameIDMatch[1],
|
|
857
|
+
attributes: { nameID: nameIDMatch[1] },
|
|
858
|
+
sessionIndex: {},
|
|
859
|
+
conditions: {}
|
|
860
|
+
} };
|
|
861
|
+
}
|
|
862
|
+
if (!parsedResponse?.extract) throw new Error("Invalid SAML response structure");
|
|
863
|
+
} catch (error) {
|
|
864
|
+
ctx.context.logger.error("SAML response validation failed", {
|
|
865
|
+
error,
|
|
866
|
+
decodedResponse: Buffer.from(SAMLResponse, "base64").toString("utf-8")
|
|
867
|
+
});
|
|
868
|
+
throw new better_auth_api.APIError("BAD_REQUEST", {
|
|
869
|
+
message: "Invalid SAML response",
|
|
870
|
+
details: error instanceof Error ? error.message : String(error)
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
const { extract } = parsedResponse;
|
|
874
|
+
const attributes = extract.attributes || {};
|
|
875
|
+
const mapping = parsedSamlConfig.mapping ?? {};
|
|
876
|
+
const userInfo = {
|
|
877
|
+
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, attributes[value]])),
|
|
878
|
+
id: attributes[mapping.id || "nameID"] || extract.nameID,
|
|
879
|
+
email: attributes[mapping.email || "email"] || extract.nameID,
|
|
880
|
+
name: [attributes[mapping.firstName || "givenName"], attributes[mapping.lastName || "surname"]].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
|
|
881
|
+
emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
|
|
882
|
+
};
|
|
883
|
+
if (!userInfo.id || !userInfo.email) {
|
|
884
|
+
ctx.context.logger.error("Missing essential user info from SAML response", {
|
|
885
|
+
attributes: Object.keys(attributes),
|
|
886
|
+
mapping,
|
|
887
|
+
extractedId: userInfo.id,
|
|
888
|
+
extractedEmail: userInfo.email
|
|
889
|
+
});
|
|
890
|
+
throw new better_auth_api.APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
|
|
891
|
+
}
|
|
892
|
+
let user;
|
|
893
|
+
const existingUser = await ctx.context.adapter.findOne({
|
|
894
|
+
model: "user",
|
|
895
|
+
where: [{
|
|
896
|
+
field: "email",
|
|
897
|
+
value: userInfo.email
|
|
898
|
+
}]
|
|
899
|
+
});
|
|
900
|
+
if (existingUser) user = existingUser;
|
|
901
|
+
else user = await ctx.context.adapter.create({
|
|
902
|
+
model: "user",
|
|
903
|
+
data: {
|
|
904
|
+
email: userInfo.email,
|
|
905
|
+
name: userInfo.name,
|
|
906
|
+
emailVerified: userInfo.emailVerified,
|
|
907
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
908
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
if (!await ctx.context.adapter.findOne({
|
|
912
|
+
model: "account",
|
|
913
|
+
where: [
|
|
914
|
+
{
|
|
915
|
+
field: "userId",
|
|
916
|
+
value: user.id
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
field: "providerId",
|
|
920
|
+
value: provider.providerId
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
field: "accountId",
|
|
924
|
+
value: userInfo.id
|
|
925
|
+
}
|
|
926
|
+
]
|
|
927
|
+
})) await ctx.context.adapter.create({
|
|
928
|
+
model: "account",
|
|
929
|
+
data: {
|
|
930
|
+
userId: user.id,
|
|
931
|
+
providerId: provider.providerId,
|
|
932
|
+
accountId: userInfo.id,
|
|
933
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
934
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
935
|
+
accessToken: "",
|
|
936
|
+
refreshToken: ""
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
if (options?.provisionUser) await options.provisionUser({
|
|
940
|
+
user,
|
|
941
|
+
userInfo,
|
|
942
|
+
provider
|
|
943
|
+
});
|
|
944
|
+
if (provider.organizationId && !options?.organizationProvisioning?.disabled) {
|
|
945
|
+
if (ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) {
|
|
946
|
+
if (!await ctx.context.adapter.findOne({
|
|
947
|
+
model: "member",
|
|
948
|
+
where: [{
|
|
949
|
+
field: "organizationId",
|
|
950
|
+
value: provider.organizationId
|
|
951
|
+
}, {
|
|
952
|
+
field: "userId",
|
|
953
|
+
value: user.id
|
|
954
|
+
}]
|
|
955
|
+
})) {
|
|
956
|
+
const role = options?.organizationProvisioning?.getRole ? await options.organizationProvisioning.getRole({
|
|
957
|
+
user,
|
|
958
|
+
userInfo,
|
|
959
|
+
provider
|
|
960
|
+
}) : options?.organizationProvisioning?.defaultRole || "member";
|
|
961
|
+
await ctx.context.adapter.create({
|
|
962
|
+
model: "member",
|
|
963
|
+
data: {
|
|
964
|
+
organizationId: provider.organizationId,
|
|
965
|
+
userId: user.id,
|
|
966
|
+
role,
|
|
967
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
968
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
await (0, better_auth_cookies.setSessionCookie)(ctx, {
|
|
975
|
+
session: await ctx.context.internalAdapter.createSession(user.id, ctx),
|
|
976
|
+
user
|
|
977
|
+
});
|
|
978
|
+
const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
979
|
+
throw ctx.redirect(callbackUrl);
|
|
980
|
+
}),
|
|
981
|
+
acsEndpoint: (0, better_auth_plugins.createAuthEndpoint)("/sso/saml2/sp/acs/:providerId", {
|
|
982
|
+
method: "POST",
|
|
983
|
+
params: zod_v4.object({ providerId: zod_v4.string().optional() }),
|
|
984
|
+
body: zod_v4.object({
|
|
985
|
+
SAMLResponse: zod_v4.string(),
|
|
986
|
+
RelayState: zod_v4.string().optional()
|
|
987
|
+
}),
|
|
988
|
+
metadata: {
|
|
989
|
+
isAction: false,
|
|
990
|
+
openapi: {
|
|
991
|
+
summary: "SAML Assertion Consumer Service",
|
|
992
|
+
description: "Handles SAML responses from IdP after successful authentication",
|
|
993
|
+
responses: { "302": { description: "Redirects to the callback URL after successful authentication" } }
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}, async (ctx) => {
|
|
997
|
+
const { SAMLResponse, RelayState = "" } = ctx.body;
|
|
998
|
+
const { providerId } = ctx.params;
|
|
999
|
+
let provider = null;
|
|
1000
|
+
if (options?.defaultSSO?.length) {
|
|
1001
|
+
const matchingDefault = providerId ? options.defaultSSO.find((defaultProvider) => defaultProvider.providerId === providerId) : options.defaultSSO[0];
|
|
1002
|
+
if (matchingDefault) provider = {
|
|
1003
|
+
issuer: matchingDefault.samlConfig?.issuer || "",
|
|
1004
|
+
providerId: matchingDefault.providerId,
|
|
1005
|
+
userId: "default",
|
|
1006
|
+
samlConfig: matchingDefault.samlConfig
|
|
1007
|
+
};
|
|
1008
|
+
} else provider = await ctx.context.adapter.findOne({
|
|
1009
|
+
model: "ssoProvider",
|
|
1010
|
+
where: [{
|
|
1011
|
+
field: "providerId",
|
|
1012
|
+
value: providerId ?? "sso"
|
|
1013
|
+
}]
|
|
1014
|
+
}).then((res) => {
|
|
1015
|
+
if (!res) return null;
|
|
1016
|
+
return {
|
|
1017
|
+
...res,
|
|
1018
|
+
samlConfig: res.samlConfig ? safeJsonParse(res.samlConfig) || void 0 : void 0
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1021
|
+
if (!provider?.samlConfig) throw new better_auth_api.APIError("NOT_FOUND", { message: "No SAML provider found" });
|
|
1022
|
+
const parsedSamlConfig = provider.samlConfig;
|
|
1023
|
+
const sp = samlify.ServiceProvider({
|
|
1024
|
+
entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
|
|
1025
|
+
assertionConsumerService: [{
|
|
1026
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1027
|
+
Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`
|
|
1028
|
+
}],
|
|
1029
|
+
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
1030
|
+
metadata: parsedSamlConfig.spMetadata?.metadata,
|
|
1031
|
+
privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
|
|
1032
|
+
privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
|
|
1033
|
+
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
1034
|
+
});
|
|
1035
|
+
const idpData = parsedSamlConfig.idpMetadata;
|
|
1036
|
+
const idp = !idpData?.metadata ? samlify.IdentityProvider({
|
|
1037
|
+
entityID: idpData?.entityID || parsedSamlConfig.issuer,
|
|
1038
|
+
singleSignOnService: idpData?.singleSignOnService || [{
|
|
1039
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
1040
|
+
Location: parsedSamlConfig.entryPoint
|
|
1041
|
+
}],
|
|
1042
|
+
signingCert: idpData?.cert || parsedSamlConfig.cert
|
|
1043
|
+
}) : samlify.IdentityProvider({ metadata: idpData.metadata });
|
|
1044
|
+
let parsedResponse;
|
|
1045
|
+
try {
|
|
1046
|
+
let decodedResponse = Buffer.from(SAMLResponse, "base64").toString("utf-8");
|
|
1047
|
+
if (!decodedResponse.includes("StatusCode")) {
|
|
1048
|
+
const insertPoint = decodedResponse.indexOf("</saml2:Issuer>");
|
|
1049
|
+
if (insertPoint !== -1) decodedResponse = decodedResponse.slice(0, insertPoint + 14) + "<saml2:Status><saml2:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></saml2:Status>" + decodedResponse.slice(insertPoint + 14);
|
|
1050
|
+
} else if (!decodedResponse.includes("saml2:Success")) decodedResponse = decodedResponse.replace(/<saml2:StatusCode Value="[^"]+"/, "<saml2:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"");
|
|
1051
|
+
try {
|
|
1052
|
+
parsedResponse = await sp.parseLoginResponse(idp, "post", { body: {
|
|
1053
|
+
SAMLResponse,
|
|
1054
|
+
RelayState: RelayState || void 0
|
|
1055
|
+
} });
|
|
1056
|
+
} catch (parseError) {
|
|
1057
|
+
const nameIDMatch = decodedResponse.match(/<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/);
|
|
1058
|
+
if (!nameIDMatch) throw parseError;
|
|
1059
|
+
parsedResponse = { extract: {
|
|
1060
|
+
nameID: nameIDMatch[1],
|
|
1061
|
+
attributes: { nameID: nameIDMatch[1] },
|
|
1062
|
+
sessionIndex: {},
|
|
1063
|
+
conditions: {}
|
|
1064
|
+
} };
|
|
1065
|
+
}
|
|
1066
|
+
if (!parsedResponse?.extract) throw new Error("Invalid SAML response structure");
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
ctx.context.logger.error("SAML response validation failed", {
|
|
1069
|
+
error,
|
|
1070
|
+
decodedResponse: Buffer.from(SAMLResponse, "base64").toString("utf-8")
|
|
1071
|
+
});
|
|
1072
|
+
throw new better_auth_api.APIError("BAD_REQUEST", {
|
|
1073
|
+
message: "Invalid SAML response",
|
|
1074
|
+
details: error instanceof Error ? error.message : String(error)
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
const { extract } = parsedResponse;
|
|
1078
|
+
const attributes = extract.attributes || {};
|
|
1079
|
+
const mapping = parsedSamlConfig.mapping ?? {};
|
|
1080
|
+
const userInfo = {
|
|
1081
|
+
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, attributes[value]])),
|
|
1082
|
+
id: attributes[mapping.id || "nameID"] || extract.nameID,
|
|
1083
|
+
email: attributes[mapping.email || "email"] || extract.nameID,
|
|
1084
|
+
name: [attributes[mapping.firstName || "givenName"], attributes[mapping.lastName || "surname"]].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
|
|
1085
|
+
emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
|
|
1086
|
+
};
|
|
1087
|
+
if (!userInfo.id || !userInfo.email) {
|
|
1088
|
+
ctx.context.logger.error("Missing essential user info from SAML response", {
|
|
1089
|
+
attributes: Object.keys(attributes),
|
|
1090
|
+
mapping,
|
|
1091
|
+
extractedId: userInfo.id,
|
|
1092
|
+
extractedEmail: userInfo.email
|
|
1093
|
+
});
|
|
1094
|
+
throw new better_auth_api.APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
|
|
1095
|
+
}
|
|
1096
|
+
let user;
|
|
1097
|
+
const existingUser = await ctx.context.adapter.findOne({
|
|
1098
|
+
model: "user",
|
|
1099
|
+
where: [{
|
|
1100
|
+
field: "email",
|
|
1101
|
+
value: userInfo.email
|
|
1102
|
+
}]
|
|
1103
|
+
});
|
|
1104
|
+
if (existingUser) {
|
|
1105
|
+
if (!await ctx.context.adapter.findOne({
|
|
1106
|
+
model: "account",
|
|
1107
|
+
where: [
|
|
1108
|
+
{
|
|
1109
|
+
field: "userId",
|
|
1110
|
+
value: existingUser.id
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
field: "providerId",
|
|
1114
|
+
value: provider.providerId
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
field: "accountId",
|
|
1118
|
+
value: userInfo.id
|
|
1119
|
+
}
|
|
1120
|
+
]
|
|
1121
|
+
})) {
|
|
1122
|
+
if (!ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId)) throw ctx.redirect(`${parsedSamlConfig.callbackUrl}?error=account_not_found`);
|
|
1123
|
+
await ctx.context.adapter.create({
|
|
1124
|
+
model: "account",
|
|
1125
|
+
data: {
|
|
1126
|
+
userId: existingUser.id,
|
|
1127
|
+
providerId: provider.providerId,
|
|
1128
|
+
accountId: userInfo.id,
|
|
1129
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1130
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1131
|
+
accessToken: "",
|
|
1132
|
+
refreshToken: ""
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
user = existingUser;
|
|
1137
|
+
} else {
|
|
1138
|
+
user = await ctx.context.adapter.create({
|
|
1139
|
+
model: "user",
|
|
1140
|
+
data: {
|
|
1141
|
+
email: userInfo.email,
|
|
1142
|
+
name: userInfo.name,
|
|
1143
|
+
emailVerified: options?.trustEmailVerified ? userInfo.emailVerified || false : false,
|
|
1144
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1145
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
await ctx.context.adapter.create({
|
|
1149
|
+
model: "account",
|
|
1150
|
+
data: {
|
|
1151
|
+
userId: user.id,
|
|
1152
|
+
providerId: provider.providerId,
|
|
1153
|
+
accountId: userInfo.id,
|
|
1154
|
+
accessToken: "",
|
|
1155
|
+
refreshToken: "",
|
|
1156
|
+
accessTokenExpiresAt: /* @__PURE__ */ new Date(),
|
|
1157
|
+
refreshTokenExpiresAt: /* @__PURE__ */ new Date(),
|
|
1158
|
+
scope: "",
|
|
1159
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1160
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (options?.provisionUser) await options.provisionUser({
|
|
1165
|
+
user,
|
|
1166
|
+
userInfo,
|
|
1167
|
+
provider
|
|
1168
|
+
});
|
|
1169
|
+
if (provider.organizationId && !options?.organizationProvisioning?.disabled) {
|
|
1170
|
+
if (ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) {
|
|
1171
|
+
if (!await ctx.context.adapter.findOne({
|
|
1172
|
+
model: "member",
|
|
1173
|
+
where: [{
|
|
1174
|
+
field: "organizationId",
|
|
1175
|
+
value: provider.organizationId
|
|
1176
|
+
}, {
|
|
1177
|
+
field: "userId",
|
|
1178
|
+
value: user.id
|
|
1179
|
+
}]
|
|
1180
|
+
})) {
|
|
1181
|
+
const role = options?.organizationProvisioning?.getRole ? await options.organizationProvisioning.getRole({
|
|
1182
|
+
user,
|
|
1183
|
+
userInfo,
|
|
1184
|
+
provider
|
|
1185
|
+
}) : options?.organizationProvisioning?.defaultRole || "member";
|
|
1186
|
+
await ctx.context.adapter.create({
|
|
1187
|
+
model: "member",
|
|
1188
|
+
data: {
|
|
1189
|
+
organizationId: provider.organizationId,
|
|
1190
|
+
userId: user.id,
|
|
1191
|
+
role,
|
|
1192
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1193
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
await (0, better_auth_cookies.setSessionCookie)(ctx, {
|
|
1200
|
+
session: await ctx.context.internalAdapter.createSession(user.id, ctx),
|
|
1201
|
+
user
|
|
1202
|
+
});
|
|
1203
|
+
const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1204
|
+
throw ctx.redirect(callbackUrl);
|
|
1205
|
+
})
|
|
1206
|
+
},
|
|
1207
|
+
schema: { ssoProvider: { fields: {
|
|
1208
|
+
issuer: {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
required: true
|
|
1211
|
+
},
|
|
1212
|
+
oidcConfig: {
|
|
1213
|
+
type: "string",
|
|
1214
|
+
required: false
|
|
1215
|
+
},
|
|
1216
|
+
samlConfig: {
|
|
1217
|
+
type: "string",
|
|
1218
|
+
required: false
|
|
1219
|
+
},
|
|
1220
|
+
userId: {
|
|
1221
|
+
type: "string",
|
|
1222
|
+
references: {
|
|
1223
|
+
model: "user",
|
|
1224
|
+
field: "id"
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
providerId: {
|
|
1228
|
+
type: "string",
|
|
1229
|
+
required: true,
|
|
1230
|
+
unique: true
|
|
1231
|
+
},
|
|
1232
|
+
organizationId: {
|
|
1233
|
+
type: "string",
|
|
1234
|
+
required: false
|
|
1235
|
+
},
|
|
1236
|
+
domain: {
|
|
1237
|
+
type: "string",
|
|
1238
|
+
required: true
|
|
1239
|
+
}
|
|
1240
|
+
} } }
|
|
1241
|
+
};
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
//#endregion
|
|
1245
|
+
Object.defineProperty(exports, 'sso', {
|
|
1246
|
+
enumerable: true,
|
|
1247
|
+
get: function () {
|
|
1248
|
+
return sso;
|
|
1249
|
+
}
|
|
1250
|
+
});
|