@better-auth/sso 1.3.17 → 1.4.0-beta.1
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 +4 -4
- package/CHANGELOG.md +20 -0
- package/dist/index.cjs +165 -551
- package/dist/index.d.cts +39 -186
- package/dist/index.d.mts +39 -186
- package/dist/index.d.ts +39 -186
- package/dist/index.mjs +165 -551
- package/package.json +5 -5
- package/src/index.ts +225 -811
- package/src/oidc.test.ts +21 -84
- package/src/saml.test.ts +8 -163
- package/tsconfig.json +15 -9
package/dist/index.cjs
CHANGED
|
@@ -76,18 +76,8 @@ const sso = (options) => {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
const parsedSamlConfig = JSON.parse(provider.samlConfig);
|
|
79
|
-
const sp =
|
|
79
|
+
const sp = saml__namespace.ServiceProvider({
|
|
80
80
|
metadata: parsedSamlConfig.spMetadata.metadata
|
|
81
|
-
}) : saml__namespace.SPMetadata({
|
|
82
|
-
entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
|
|
83
|
-
assertionConsumerService: [
|
|
84
|
-
{
|
|
85
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
86
|
-
Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${provider.id}`
|
|
87
|
-
}
|
|
88
|
-
],
|
|
89
|
-
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
90
|
-
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
91
81
|
});
|
|
92
82
|
return new Response(sp.getMetadata(), {
|
|
93
83
|
headers: {
|
|
@@ -101,57 +91,64 @@ const sso = (options) => {
|
|
|
101
91
|
{
|
|
102
92
|
method: "POST",
|
|
103
93
|
body: z__namespace.object({
|
|
104
|
-
providerId: z__namespace.string({}).
|
|
105
|
-
"The ID of the provider. This is used to identify the provider during login and callback"
|
|
106
|
-
),
|
|
107
|
-
issuer: z__namespace.string({}).
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
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
|
+
}),
|
|
111
103
|
oidcConfig: z__namespace.object({
|
|
112
|
-
clientId: z__namespace.string({}).
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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(),
|
|
117
119
|
tokenEndpointAuthentication: z__namespace.enum(["client_secret_post", "client_secret_basic"]).optional(),
|
|
118
|
-
jwksEndpoint: z__namespace.string({}).
|
|
120
|
+
jwksEndpoint: z__namespace.string({}).meta({
|
|
121
|
+
description: "The JWKS endpoint"
|
|
122
|
+
}).optional(),
|
|
119
123
|
discoveryEndpoint: z__namespace.string().optional(),
|
|
120
|
-
scopes: z__namespace.array(z__namespace.string(), {}).
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
name: z__namespace.string({}).describe("Field mapping for name ("),
|
|
127
|
-
image: z__namespace.string({}).describe("Field mapping for image (").optional(),
|
|
128
|
-
extraFields: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
|
|
129
|
-
}).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
130
|
}).optional(),
|
|
131
131
|
samlConfig: z__namespace.object({
|
|
132
|
-
entryPoint: z__namespace.string({}).
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}),
|
|
135
141
|
audience: z__namespace.string().optional(),
|
|
136
142
|
idpMetadata: z__namespace.object({
|
|
137
|
-
metadata: z__namespace.string()
|
|
138
|
-
entityID: z__namespace.string().optional(),
|
|
139
|
-
cert: z__namespace.string().optional(),
|
|
143
|
+
metadata: z__namespace.string(),
|
|
140
144
|
privateKey: z__namespace.string().optional(),
|
|
141
145
|
privateKeyPass: z__namespace.string().optional(),
|
|
142
146
|
isAssertionEncrypted: z__namespace.boolean().optional(),
|
|
143
147
|
encPrivateKey: z__namespace.string().optional(),
|
|
144
|
-
encPrivateKeyPass: z__namespace.string().optional()
|
|
145
|
-
singleSignOnService: z__namespace.array(
|
|
146
|
-
z__namespace.object({
|
|
147
|
-
Binding: z__namespace.string().describe("The binding type for the SSO service"),
|
|
148
|
-
Location: z__namespace.string().describe("The URL for the SSO service")
|
|
149
|
-
})
|
|
150
|
-
).optional().describe("Single Sign-On service configuration")
|
|
148
|
+
encPrivateKeyPass: z__namespace.string().optional()
|
|
151
149
|
}).optional(),
|
|
152
150
|
spMetadata: z__namespace.object({
|
|
153
|
-
metadata: z__namespace.string()
|
|
154
|
-
entityID: z__namespace.string().optional(),
|
|
151
|
+
metadata: z__namespace.string(),
|
|
155
152
|
binding: z__namespace.string().optional(),
|
|
156
153
|
privateKey: z__namespace.string().optional(),
|
|
157
154
|
privateKeyPass: z__namespace.string().optional(),
|
|
@@ -165,23 +162,32 @@ const sso = (options) => {
|
|
|
165
162
|
identifierFormat: z__namespace.string().optional(),
|
|
166
163
|
privateKey: z__namespace.string().optional(),
|
|
167
164
|
decryptionPvk: z__namespace.string().optional(),
|
|
168
|
-
additionalParams: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
|
|
169
|
-
mapping: z__namespace.object({
|
|
170
|
-
id: z__namespace.string({}).describe("Field mapping for user ID ("),
|
|
171
|
-
email: z__namespace.string({}).describe("Field mapping for email ("),
|
|
172
|
-
emailVerified: z__namespace.string({}).describe("Field mapping for email verification").optional(),
|
|
173
|
-
name: z__namespace.string({}).describe("Field mapping for name ("),
|
|
174
|
-
firstName: z__namespace.string({}).describe("Field mapping for first name (").optional(),
|
|
175
|
-
lastName: z__namespace.string({}).describe("Field mapping for last name (").optional(),
|
|
176
|
-
extraFields: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
|
|
177
|
-
}).optional()
|
|
165
|
+
additionalParams: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
|
|
178
166
|
}).optional(),
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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()
|
|
185
191
|
}),
|
|
186
192
|
use: [api.sessionMiddleware],
|
|
187
193
|
metadata: {
|
|
@@ -411,7 +417,7 @@ const sso = (options) => {
|
|
|
411
417
|
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
412
418
|
pkce: body.oidcConfig.pkce,
|
|
413
419
|
discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
|
|
414
|
-
mapping: body.
|
|
420
|
+
mapping: body.mapping,
|
|
415
421
|
scopes: body.oidcConfig.scopes,
|
|
416
422
|
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
417
423
|
overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
|
|
@@ -431,7 +437,7 @@ const sso = (options) => {
|
|
|
431
437
|
privateKey: body.samlConfig.privateKey,
|
|
432
438
|
decryptionPvk: body.samlConfig.decryptionPvk,
|
|
433
439
|
additionalParams: body.samlConfig.additionalParams,
|
|
434
|
-
mapping: body.
|
|
440
|
+
mapping: body.mapping
|
|
435
441
|
}) : null,
|
|
436
442
|
organizationId: body.organizationId,
|
|
437
443
|
userId: ctx.context.session.user.id,
|
|
@@ -455,21 +461,33 @@ const sso = (options) => {
|
|
|
455
461
|
{
|
|
456
462
|
method: "POST",
|
|
457
463
|
body: z__namespace.object({
|
|
458
|
-
email: z__namespace.string({}).
|
|
459
|
-
"The email address to sign in with. This is used to identify the issuer to sign in with"
|
|
460
|
-
).optional(),
|
|
461
|
-
organizationSlug: z__namespace.string({}).
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
).
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"
|
|
472
|
-
)
|
|
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(),
|
|
473
491
|
providerType: z__namespace.enum(["oidc", "saml"]).optional()
|
|
474
492
|
}),
|
|
475
493
|
metadata: {
|
|
@@ -543,7 +561,7 @@ const sso = (options) => {
|
|
|
543
561
|
async (ctx) => {
|
|
544
562
|
const body = ctx.body;
|
|
545
563
|
let { email, organizationSlug, providerId, domain } = body;
|
|
546
|
-
if (!
|
|
564
|
+
if (!email && !organizationSlug && !domain && !providerId) {
|
|
547
565
|
throw new api.APIError("BAD_REQUEST", {
|
|
548
566
|
message: "email, organizationSlug, domain or providerId is required"
|
|
549
567
|
});
|
|
@@ -566,48 +584,23 @@ const sso = (options) => {
|
|
|
566
584
|
return res.id;
|
|
567
585
|
});
|
|
568
586
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
);
|
|
576
|
-
if (matchingDefault) {
|
|
577
|
-
provider = {
|
|
578
|
-
issuer: matchingDefault.samlConfig?.issuer || matchingDefault.oidcConfig?.issuer || "",
|
|
579
|
-
providerId: matchingDefault.providerId,
|
|
580
|
-
userId: "default",
|
|
581
|
-
oidcConfig: matchingDefault.oidcConfig,
|
|
582
|
-
samlConfig: matchingDefault.samlConfig
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
if (!providerId && !orgId && !domain) {
|
|
587
|
-
throw new api.APIError("BAD_REQUEST", {
|
|
588
|
-
message: "providerId, orgId or domain is required"
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
if (!provider) {
|
|
592
|
-
provider = await ctx.context.adapter.findOne({
|
|
593
|
-
model: "ssoProvider",
|
|
594
|
-
where: [
|
|
595
|
-
{
|
|
596
|
-
field: providerId ? "providerId" : orgId ? "organizationId" : "domain",
|
|
597
|
-
value: providerId || orgId || domain
|
|
598
|
-
}
|
|
599
|
-
]
|
|
600
|
-
}).then((res) => {
|
|
601
|
-
if (!res) {
|
|
602
|
-
return null;
|
|
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
|
|
603
593
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
594
|
+
]
|
|
595
|
+
}).then((res) => {
|
|
596
|
+
if (!res) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
...res,
|
|
601
|
+
oidcConfig: JSON.parse(res.oidcConfig)
|
|
602
|
+
};
|
|
603
|
+
});
|
|
611
604
|
if (!provider) {
|
|
612
605
|
throw new api.APIError("NOT_FOUND", {
|
|
613
606
|
message: "No provider found for the issuer"
|
|
@@ -651,16 +644,15 @@ const sso = (options) => {
|
|
|
651
644
|
});
|
|
652
645
|
}
|
|
653
646
|
if (provider.samlConfig) {
|
|
654
|
-
const parsedSamlConfig =
|
|
647
|
+
const parsedSamlConfig = JSON.parse(
|
|
648
|
+
provider.samlConfig
|
|
649
|
+
);
|
|
655
650
|
const sp = saml__namespace.ServiceProvider({
|
|
656
651
|
metadata: parsedSamlConfig.spMetadata.metadata,
|
|
657
652
|
allowCreate: true
|
|
658
653
|
});
|
|
659
654
|
const idp = saml__namespace.IdentityProvider({
|
|
660
|
-
metadata: parsedSamlConfig.idpMetadata
|
|
661
|
-
entityID: parsedSamlConfig.idpMetadata?.entityID,
|
|
662
|
-
encryptCert: parsedSamlConfig.idpMetadata?.cert,
|
|
663
|
-
singleSignOnService: parsedSamlConfig.idpMetadata?.singleSignOnService
|
|
655
|
+
metadata: parsedSamlConfig.idpMetadata.metadata
|
|
664
656
|
});
|
|
665
657
|
const loginRequest = sp.createLoginRequest(
|
|
666
658
|
idp,
|
|
@@ -719,38 +711,23 @@ const sso = (options) => {
|
|
|
719
711
|
`${errorURL || callbackURL}?error=${error}&error_description=${error_description}`
|
|
720
712
|
);
|
|
721
713
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
provider = {
|
|
729
|
-
...matchingDefault,
|
|
730
|
-
issuer: matchingDefault.oidcConfig?.issuer || "",
|
|
731
|
-
userId: "default"
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
if (!provider) {
|
|
736
|
-
provider = await ctx.context.adapter.findOne({
|
|
737
|
-
model: "ssoProvider",
|
|
738
|
-
where: [
|
|
739
|
-
{
|
|
740
|
-
field: "providerId",
|
|
741
|
-
value: ctx.params.providerId
|
|
742
|
-
}
|
|
743
|
-
]
|
|
744
|
-
}).then((res) => {
|
|
745
|
-
if (!res) {
|
|
746
|
-
return null;
|
|
714
|
+
const provider = await ctx.context.adapter.findOne({
|
|
715
|
+
model: "ssoProvider",
|
|
716
|
+
where: [
|
|
717
|
+
{
|
|
718
|
+
field: "providerId",
|
|
719
|
+
value: ctx.params.providerId
|
|
747
720
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
|
|
721
|
+
]
|
|
722
|
+
}).then((res) => {
|
|
723
|
+
if (!res) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
...res,
|
|
728
|
+
oidcConfig: JSON.parse(res.oidcConfig)
|
|
729
|
+
};
|
|
730
|
+
});
|
|
754
731
|
if (!provider) {
|
|
755
732
|
throw ctx.redirect(
|
|
756
733
|
`${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`
|
|
@@ -974,31 +951,10 @@ const sso = (options) => {
|
|
|
974
951
|
async (ctx) => {
|
|
975
952
|
const { SAMLResponse, RelayState } = ctx.body;
|
|
976
953
|
const { providerId } = ctx.params;
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
);
|
|
982
|
-
if (matchingDefault) {
|
|
983
|
-
provider = {
|
|
984
|
-
...matchingDefault,
|
|
985
|
-
userId: "default",
|
|
986
|
-
issuer: matchingDefault.samlConfig?.issuer || ""
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
if (!provider) {
|
|
991
|
-
provider = await ctx.context.adapter.findOne({
|
|
992
|
-
model: "ssoProvider",
|
|
993
|
-
where: [{ field: "providerId", value: providerId }]
|
|
994
|
-
}).then((res) => {
|
|
995
|
-
if (!res) return null;
|
|
996
|
-
return {
|
|
997
|
-
...res,
|
|
998
|
-
samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
|
|
999
|
-
};
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
954
|
+
const provider = await ctx.context.adapter.findOne({
|
|
955
|
+
model: "ssoProvider",
|
|
956
|
+
where: [{ field: "providerId", value: providerId }]
|
|
957
|
+
});
|
|
1002
958
|
if (!provider) {
|
|
1003
959
|
throw new api.APIError("NOT_FOUND", {
|
|
1004
960
|
message: "No provider found for the given providerId"
|
|
@@ -1007,389 +963,46 @@ const sso = (options) => {
|
|
|
1007
963
|
const parsedSamlConfig = JSON.parse(
|
|
1008
964
|
provider.samlConfig
|
|
1009
965
|
);
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
if (!idpData?.metadata) {
|
|
1013
|
-
idp = saml__namespace.IdentityProvider({
|
|
1014
|
-
entityID: idpData.entityID || parsedSamlConfig.issuer,
|
|
1015
|
-
singleSignOnService: [
|
|
1016
|
-
{
|
|
1017
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
1018
|
-
Location: parsedSamlConfig.entryPoint
|
|
1019
|
-
}
|
|
1020
|
-
],
|
|
1021
|
-
signingCert: idpData.cert || parsedSamlConfig.cert,
|
|
1022
|
-
wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
1023
|
-
isAssertionEncrypted: idpData.isAssertionEncrypted || false,
|
|
1024
|
-
encPrivateKey: idpData.encPrivateKey,
|
|
1025
|
-
encPrivateKeyPass: idpData.encPrivateKeyPass
|
|
1026
|
-
});
|
|
1027
|
-
} else {
|
|
1028
|
-
idp = saml__namespace.IdentityProvider({
|
|
1029
|
-
metadata: idpData.metadata,
|
|
1030
|
-
privateKey: idpData.privateKey,
|
|
1031
|
-
privateKeyPass: idpData.privateKeyPass,
|
|
1032
|
-
isAssertionEncrypted: idpData.isAssertionEncrypted,
|
|
1033
|
-
encPrivateKey: idpData.encPrivateKey,
|
|
1034
|
-
encPrivateKeyPass: idpData.encPrivateKeyPass
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
const spData = parsedSamlConfig.spMetadata;
|
|
1038
|
-
const sp = saml__namespace.ServiceProvider({
|
|
1039
|
-
metadata: spData?.metadata,
|
|
1040
|
-
entityID: spData?.entityID || parsedSamlConfig.issuer,
|
|
1041
|
-
assertionConsumerService: spData?.metadata ? void 0 : [
|
|
1042
|
-
{
|
|
1043
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1044
|
-
Location: parsedSamlConfig.callbackUrl
|
|
1045
|
-
}
|
|
1046
|
-
],
|
|
1047
|
-
privateKey: spData?.privateKey || parsedSamlConfig.privateKey,
|
|
1048
|
-
privateKeyPass: spData?.privateKeyPass,
|
|
1049
|
-
isAssertionEncrypted: spData?.isAssertionEncrypted || false,
|
|
1050
|
-
encPrivateKey: spData?.encPrivateKey,
|
|
1051
|
-
encPrivateKeyPass: spData?.encPrivateKeyPass,
|
|
1052
|
-
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
1053
|
-
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
966
|
+
const idp = saml__namespace.IdentityProvider({
|
|
967
|
+
metadata: parsedSamlConfig.idpMetadata.metadata
|
|
1054
968
|
});
|
|
1055
|
-
let parsedResponse;
|
|
1056
|
-
try {
|
|
1057
|
-
const decodedResponse = Buffer.from(
|
|
1058
|
-
SAMLResponse,
|
|
1059
|
-
"base64"
|
|
1060
|
-
).toString("utf-8");
|
|
1061
|
-
try {
|
|
1062
|
-
parsedResponse = await sp.parseLoginResponse(idp, "post", {
|
|
1063
|
-
body: {
|
|
1064
|
-
SAMLResponse,
|
|
1065
|
-
RelayState: RelayState || void 0
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
} catch (parseError) {
|
|
1069
|
-
const nameIDMatch = decodedResponse.match(
|
|
1070
|
-
/<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
|
|
1071
|
-
);
|
|
1072
|
-
if (!nameIDMatch) throw parseError;
|
|
1073
|
-
parsedResponse = {
|
|
1074
|
-
extract: {
|
|
1075
|
-
nameID: nameIDMatch[1],
|
|
1076
|
-
attributes: { nameID: nameIDMatch[1] },
|
|
1077
|
-
sessionIndex: {},
|
|
1078
|
-
conditions: {}
|
|
1079
|
-
}
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
if (!parsedResponse?.extract) {
|
|
1083
|
-
throw new Error("Invalid SAML response structure");
|
|
1084
|
-
}
|
|
1085
|
-
} catch (error) {
|
|
1086
|
-
ctx.context.logger.error("SAML response validation failed", {
|
|
1087
|
-
error,
|
|
1088
|
-
decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
|
|
1089
|
-
"utf-8"
|
|
1090
|
-
)
|
|
1091
|
-
});
|
|
1092
|
-
throw new api.APIError("BAD_REQUEST", {
|
|
1093
|
-
message: "Invalid SAML response",
|
|
1094
|
-
details: error instanceof Error ? error.message : String(error)
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
const { extract } = parsedResponse;
|
|
1098
|
-
const attributes = extract.attributes || {};
|
|
1099
|
-
const mapping = parsedSamlConfig.mapping ?? {};
|
|
1100
|
-
const userInfo = {
|
|
1101
|
-
...Object.fromEntries(
|
|
1102
|
-
Object.entries(mapping.extraFields || {}).map(([key, value]) => [
|
|
1103
|
-
key,
|
|
1104
|
-
attributes[value]
|
|
1105
|
-
])
|
|
1106
|
-
),
|
|
1107
|
-
id: attributes[mapping.id || "nameID"] || extract.nameID,
|
|
1108
|
-
email: attributes[mapping.email || "email"] || extract.nameID,
|
|
1109
|
-
name: [
|
|
1110
|
-
attributes[mapping.firstName || "givenName"],
|
|
1111
|
-
attributes[mapping.lastName || "surname"]
|
|
1112
|
-
].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
|
|
1113
|
-
emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
|
|
1114
|
-
};
|
|
1115
|
-
if (!userInfo.id || !userInfo.email) {
|
|
1116
|
-
ctx.context.logger.error(
|
|
1117
|
-
"Missing essential user info from SAML response",
|
|
1118
|
-
{
|
|
1119
|
-
attributes: Object.keys(attributes),
|
|
1120
|
-
mapping,
|
|
1121
|
-
extractedId: userInfo.id,
|
|
1122
|
-
extractedEmail: userInfo.email
|
|
1123
|
-
}
|
|
1124
|
-
);
|
|
1125
|
-
throw new api.APIError("BAD_REQUEST", {
|
|
1126
|
-
message: "Unable to extract user ID or email from SAML response"
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
let user;
|
|
1130
|
-
const existingUser = await ctx.context.adapter.findOne({
|
|
1131
|
-
model: "user",
|
|
1132
|
-
where: [
|
|
1133
|
-
{
|
|
1134
|
-
field: "email",
|
|
1135
|
-
value: userInfo.email
|
|
1136
|
-
}
|
|
1137
|
-
]
|
|
1138
|
-
});
|
|
1139
|
-
if (existingUser) {
|
|
1140
|
-
user = existingUser;
|
|
1141
|
-
} else {
|
|
1142
|
-
user = await ctx.context.adapter.create({
|
|
1143
|
-
model: "user",
|
|
1144
|
-
data: {
|
|
1145
|
-
email: userInfo.email,
|
|
1146
|
-
name: userInfo.name,
|
|
1147
|
-
emailVerified: userInfo.emailVerified,
|
|
1148
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1149
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1150
|
-
}
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
const account = await ctx.context.adapter.findOne({
|
|
1154
|
-
model: "account",
|
|
1155
|
-
where: [
|
|
1156
|
-
{ field: "userId", value: user.id },
|
|
1157
|
-
{ field: "providerId", value: provider.providerId },
|
|
1158
|
-
{ field: "accountId", value: userInfo.id }
|
|
1159
|
-
]
|
|
1160
|
-
});
|
|
1161
|
-
if (!account) {
|
|
1162
|
-
await ctx.context.adapter.create({
|
|
1163
|
-
model: "account",
|
|
1164
|
-
data: {
|
|
1165
|
-
userId: user.id,
|
|
1166
|
-
providerId: provider.providerId,
|
|
1167
|
-
accountId: userInfo.id,
|
|
1168
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1169
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1170
|
-
accessToken: "",
|
|
1171
|
-
refreshToken: ""
|
|
1172
|
-
}
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
if (options?.provisionUser) {
|
|
1176
|
-
await options.provisionUser({
|
|
1177
|
-
user,
|
|
1178
|
-
userInfo,
|
|
1179
|
-
provider
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
if (provider.organizationId && !options?.organizationProvisioning?.disabled) {
|
|
1183
|
-
const isOrgPluginEnabled = ctx.context.options.plugins?.find(
|
|
1184
|
-
(plugin) => plugin.id === "organization"
|
|
1185
|
-
);
|
|
1186
|
-
if (isOrgPluginEnabled) {
|
|
1187
|
-
const isAlreadyMember = await ctx.context.adapter.findOne({
|
|
1188
|
-
model: "member",
|
|
1189
|
-
where: [
|
|
1190
|
-
{ field: "organizationId", value: provider.organizationId },
|
|
1191
|
-
{ field: "userId", value: user.id }
|
|
1192
|
-
]
|
|
1193
|
-
});
|
|
1194
|
-
if (!isAlreadyMember) {
|
|
1195
|
-
const role = options?.organizationProvisioning?.getRole ? await options.organizationProvisioning.getRole({
|
|
1196
|
-
user,
|
|
1197
|
-
userInfo,
|
|
1198
|
-
provider
|
|
1199
|
-
}) : options?.organizationProvisioning?.defaultRole || "member";
|
|
1200
|
-
await ctx.context.adapter.create({
|
|
1201
|
-
model: "member",
|
|
1202
|
-
data: {
|
|
1203
|
-
organizationId: provider.organizationId,
|
|
1204
|
-
userId: user.id,
|
|
1205
|
-
role,
|
|
1206
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1207
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1208
|
-
}
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
|
|
1214
|
-
await cookies.setSessionCookie(ctx, { session, user });
|
|
1215
|
-
const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1216
|
-
throw ctx.redirect(callbackUrl);
|
|
1217
|
-
}
|
|
1218
|
-
),
|
|
1219
|
-
acsEndpoint: plugins.createAuthEndpoint(
|
|
1220
|
-
"/sso/saml2/sp/acs/:providerId",
|
|
1221
|
-
{
|
|
1222
|
-
method: "POST",
|
|
1223
|
-
params: z__namespace.object({
|
|
1224
|
-
providerId: z__namespace.string().optional()
|
|
1225
|
-
}),
|
|
1226
|
-
body: z__namespace.object({
|
|
1227
|
-
SAMLResponse: z__namespace.string(),
|
|
1228
|
-
RelayState: z__namespace.string().optional()
|
|
1229
|
-
}),
|
|
1230
|
-
metadata: {
|
|
1231
|
-
isAction: false,
|
|
1232
|
-
openapi: {
|
|
1233
|
-
summary: "SAML Assertion Consumer Service",
|
|
1234
|
-
description: "Handles SAML responses from IdP after successful authentication",
|
|
1235
|
-
responses: {
|
|
1236
|
-
"302": {
|
|
1237
|
-
description: "Redirects to the callback URL after successful authentication"
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
},
|
|
1243
|
-
async (ctx) => {
|
|
1244
|
-
const { SAMLResponse, RelayState = "" } = ctx.body;
|
|
1245
|
-
const { providerId } = ctx.params;
|
|
1246
|
-
let provider = null;
|
|
1247
|
-
if (options?.defaultSSO?.length) {
|
|
1248
|
-
const matchingDefault = providerId ? options.defaultSSO.find(
|
|
1249
|
-
(defaultProvider) => defaultProvider.providerId === providerId
|
|
1250
|
-
) : options.defaultSSO[0];
|
|
1251
|
-
if (matchingDefault) {
|
|
1252
|
-
provider = {
|
|
1253
|
-
issuer: matchingDefault.samlConfig?.issuer || "",
|
|
1254
|
-
providerId: matchingDefault.providerId,
|
|
1255
|
-
userId: "default",
|
|
1256
|
-
samlConfig: matchingDefault.samlConfig
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
} else {
|
|
1260
|
-
provider = await ctx.context.adapter.findOne({
|
|
1261
|
-
model: "ssoProvider",
|
|
1262
|
-
where: [
|
|
1263
|
-
{
|
|
1264
|
-
field: "providerId",
|
|
1265
|
-
value: providerId ?? "sso"
|
|
1266
|
-
}
|
|
1267
|
-
]
|
|
1268
|
-
}).then((res) => {
|
|
1269
|
-
if (!res) return null;
|
|
1270
|
-
return {
|
|
1271
|
-
...res,
|
|
1272
|
-
samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
|
|
1273
|
-
};
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
if (!provider?.samlConfig) {
|
|
1277
|
-
throw new api.APIError("NOT_FOUND", {
|
|
1278
|
-
message: "No SAML provider found"
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
const parsedSamlConfig = provider.samlConfig;
|
|
1282
969
|
const sp = saml__namespace.ServiceProvider({
|
|
1283
|
-
|
|
1284
|
-
assertionConsumerService: [
|
|
1285
|
-
{
|
|
1286
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1287
|
-
Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`
|
|
1288
|
-
}
|
|
1289
|
-
],
|
|
1290
|
-
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
1291
|
-
metadata: parsedSamlConfig.spMetadata?.metadata,
|
|
1292
|
-
privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
|
|
1293
|
-
privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
|
|
1294
|
-
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
1295
|
-
});
|
|
1296
|
-
const idpData = parsedSamlConfig.idpMetadata;
|
|
1297
|
-
const idp = !idpData?.metadata ? saml__namespace.IdentityProvider({
|
|
1298
|
-
entityID: idpData?.entityID || parsedSamlConfig.issuer,
|
|
1299
|
-
singleSignOnService: idpData?.singleSignOnService || [
|
|
1300
|
-
{
|
|
1301
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
1302
|
-
Location: parsedSamlConfig.entryPoint
|
|
1303
|
-
}
|
|
1304
|
-
],
|
|
1305
|
-
signingCert: idpData?.cert || parsedSamlConfig.cert
|
|
1306
|
-
}) : saml__namespace.IdentityProvider({
|
|
1307
|
-
metadata: idpData.metadata
|
|
970
|
+
metadata: parsedSamlConfig.spMetadata.metadata
|
|
1308
971
|
});
|
|
1309
972
|
let parsedResponse;
|
|
1310
973
|
try {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
);
|
|
1314
|
-
if (!
|
|
1315
|
-
|
|
1316
|
-
if (insertPoint !== -1) {
|
|
1317
|
-
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);
|
|
1318
|
-
}
|
|
1319
|
-
} else if (!decodedResponse.includes("saml2:Success")) {
|
|
1320
|
-
decodedResponse = decodedResponse.replace(
|
|
1321
|
-
/<saml2:StatusCode Value="[^"]+"/,
|
|
1322
|
-
'<saml2:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"'
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
try {
|
|
1326
|
-
parsedResponse = await sp.parseLoginResponse(idp, "post", {
|
|
1327
|
-
body: {
|
|
1328
|
-
SAMLResponse,
|
|
1329
|
-
RelayState: RelayState || void 0
|
|
1330
|
-
}
|
|
1331
|
-
});
|
|
1332
|
-
} catch (parseError) {
|
|
1333
|
-
const nameIDMatch = decodedResponse.match(
|
|
1334
|
-
/<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
|
|
1335
|
-
);
|
|
1336
|
-
if (!nameIDMatch) throw parseError;
|
|
1337
|
-
parsedResponse = {
|
|
1338
|
-
extract: {
|
|
1339
|
-
nameID: nameIDMatch[1],
|
|
1340
|
-
attributes: { nameID: nameIDMatch[1] },
|
|
1341
|
-
sessionIndex: {},
|
|
1342
|
-
conditions: {}
|
|
1343
|
-
}
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
if (!parsedResponse?.extract) {
|
|
1347
|
-
throw new Error("Invalid SAML response structure");
|
|
974
|
+
parsedResponse = await sp.parseLoginResponse(idp, "post", {
|
|
975
|
+
body: { SAMLResponse, RelayState }
|
|
976
|
+
});
|
|
977
|
+
if (!parsedResponse) {
|
|
978
|
+
throw new Error("Empty SAML response");
|
|
1348
979
|
}
|
|
1349
980
|
} catch (error) {
|
|
1350
|
-
ctx.context.logger.error("SAML response validation failed",
|
|
1351
|
-
error,
|
|
1352
|
-
decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
|
|
1353
|
-
"utf-8"
|
|
1354
|
-
)
|
|
1355
|
-
});
|
|
981
|
+
ctx.context.logger.error("SAML response validation failed", error);
|
|
1356
982
|
throw new api.APIError("BAD_REQUEST", {
|
|
1357
983
|
message: "Invalid SAML response",
|
|
1358
984
|
details: error instanceof Error ? error.message : String(error)
|
|
1359
985
|
});
|
|
1360
986
|
}
|
|
1361
987
|
const { extract } = parsedResponse;
|
|
1362
|
-
const attributes = extract.attributes
|
|
1363
|
-
const mapping = parsedSamlConfig
|
|
988
|
+
const attributes = parsedResponse.extract.attributes;
|
|
989
|
+
const mapping = parsedSamlConfig?.mapping ?? {};
|
|
1364
990
|
const userInfo = {
|
|
1365
991
|
...Object.fromEntries(
|
|
1366
992
|
Object.entries(mapping.extraFields || {}).map(([key, value]) => [
|
|
1367
993
|
key,
|
|
1368
|
-
attributes[value]
|
|
994
|
+
extract.attributes[value]
|
|
1369
995
|
])
|
|
1370
996
|
),
|
|
1371
|
-
id: attributes[mapping.id || "nameID"]
|
|
1372
|
-
email: attributes[mapping.email || "
|
|
997
|
+
id: attributes[mapping.id] || attributes["nameID"],
|
|
998
|
+
email: attributes[mapping.email] || attributes["nameID"] || attributes["email"],
|
|
1373
999
|
name: [
|
|
1374
|
-
attributes[mapping.firstName || "givenName"],
|
|
1375
|
-
attributes[mapping.lastName || "surname"]
|
|
1376
|
-
].filter(Boolean).join(" ") ||
|
|
1377
|
-
|
|
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
|
|
1378
1005
|
};
|
|
1379
|
-
if (!userInfo.id || !userInfo.email) {
|
|
1380
|
-
ctx.context.logger.error(
|
|
1381
|
-
"Missing essential user info from SAML response",
|
|
1382
|
-
{
|
|
1383
|
-
attributes: Object.keys(attributes),
|
|
1384
|
-
mapping,
|
|
1385
|
-
extractedId: userInfo.id,
|
|
1386
|
-
extractedEmail: userInfo.email
|
|
1387
|
-
}
|
|
1388
|
-
);
|
|
1389
|
-
throw new api.APIError("BAD_REQUEST", {
|
|
1390
|
-
message: "Unable to extract user ID or email from SAML response"
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
1006
|
let user;
|
|
1394
1007
|
const existingUser = await ctx.context.adapter.findOne({
|
|
1395
1008
|
model: "user",
|
|
@@ -1401,7 +1014,7 @@ const sso = (options) => {
|
|
|
1401
1014
|
]
|
|
1402
1015
|
});
|
|
1403
1016
|
if (existingUser) {
|
|
1404
|
-
const
|
|
1017
|
+
const accounts = await ctx.context.adapter.findOne({
|
|
1405
1018
|
model: "account",
|
|
1406
1019
|
where: [
|
|
1407
1020
|
{ field: "userId", value: existingUser.id },
|
|
@@ -1409,7 +1022,7 @@ const sso = (options) => {
|
|
|
1409
1022
|
{ field: "accountId", value: userInfo.id }
|
|
1410
1023
|
]
|
|
1411
1024
|
});
|
|
1412
|
-
if (!
|
|
1025
|
+
if (!accounts) {
|
|
1413
1026
|
const isTrustedProvider = ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
|
|
1414
1027
|
provider.providerId
|
|
1415
1028
|
);
|
|
@@ -1499,8 +1112,9 @@ const sso = (options) => {
|
|
|
1499
1112
|
}
|
|
1500
1113
|
let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
|
|
1501
1114
|
await cookies.setSessionCookie(ctx, { session, user });
|
|
1502
|
-
|
|
1503
|
-
|
|
1115
|
+
throw ctx.redirect(
|
|
1116
|
+
RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
|
|
1117
|
+
);
|
|
1504
1118
|
}
|
|
1505
1119
|
)
|
|
1506
1120
|
},
|