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