@better-auth/sso 1.3.0-beta.8 → 1.3.0
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/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/index.cjs +111 -84
- package/dist/index.d.cts +57 -319
- package/dist/index.d.mts +57 -319
- package/dist/index.d.ts +57 -319
- package/dist/index.mjs +68 -42
- package/package.json +2 -2
- package/src/index.ts +115 -42
- package/src/oidc.test.ts +101 -0
- package/src/saml.test.ts +292 -107
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { generateState } from 'better-auth';
|
|
|
2
2
|
import { APIError, sessionMiddleware } from 'better-auth/api';
|
|
3
3
|
import { parseState, validateAuthorizationCode, validateToken, handleOAuthUserInfo, createAuthorizationURL } from 'better-auth/oauth2';
|
|
4
4
|
import { createAuthEndpoint } from 'better-auth/plugins';
|
|
5
|
-
import
|
|
5
|
+
import * as z from 'zod/v4';
|
|
6
6
|
import * as saml from 'samlify';
|
|
7
7
|
import { betterFetch, BetterFetchError } from '@better-fetch/fetch';
|
|
8
8
|
import { decodeJwt } from 'jose';
|
|
@@ -74,47 +74,53 @@ const sso = (options) => {
|
|
|
74
74
|
{
|
|
75
75
|
method: "POST",
|
|
76
76
|
body: z.object({
|
|
77
|
-
providerId: z.string({
|
|
77
|
+
providerId: z.string({}).meta({
|
|
78
78
|
description: "The ID of the provider. This is used to identify the provider during login and callback"
|
|
79
79
|
}),
|
|
80
|
-
issuer: z.string({
|
|
80
|
+
issuer: z.string({}).meta({
|
|
81
81
|
description: "The issuer of the provider"
|
|
82
82
|
}),
|
|
83
|
-
domain: z.string({
|
|
83
|
+
domain: z.string({}).meta({
|
|
84
84
|
description: "The domain of the provider. This is used for email matching"
|
|
85
85
|
}),
|
|
86
86
|
oidcConfig: z.object({
|
|
87
|
-
clientId: z.string({
|
|
87
|
+
clientId: z.string({}).meta({
|
|
88
88
|
description: "The client ID"
|
|
89
89
|
}),
|
|
90
|
-
clientSecret: z.string({
|
|
90
|
+
clientSecret: z.string({}).meta({
|
|
91
91
|
description: "The client secret"
|
|
92
92
|
}),
|
|
93
|
-
authorizationEndpoint: z.string({
|
|
93
|
+
authorizationEndpoint: z.string({}).meta({
|
|
94
94
|
description: "The authorization endpoint"
|
|
95
95
|
}).optional(),
|
|
96
|
-
tokenEndpoint: z.string({
|
|
96
|
+
tokenEndpoint: z.string({}).meta({
|
|
97
97
|
description: "The token endpoint"
|
|
98
98
|
}).optional(),
|
|
99
|
-
userInfoEndpoint: z.string({
|
|
99
|
+
userInfoEndpoint: z.string({}).meta({
|
|
100
100
|
description: "The user info endpoint"
|
|
101
101
|
}).optional(),
|
|
102
102
|
tokenEndpointAuthentication: z.enum(["client_secret_post", "client_secret_basic"]).optional(),
|
|
103
|
-
jwksEndpoint: z.string({
|
|
103
|
+
jwksEndpoint: z.string({}).meta({
|
|
104
104
|
description: "The JWKS endpoint"
|
|
105
105
|
}).optional(),
|
|
106
106
|
discoveryEndpoint: z.string().optional(),
|
|
107
|
-
scopes: z.array(z.string(), {
|
|
107
|
+
scopes: z.array(z.string(), {}).meta({
|
|
108
108
|
description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']"
|
|
109
109
|
}).optional(),
|
|
110
|
-
pkce: z.boolean({
|
|
110
|
+
pkce: z.boolean({}).meta({
|
|
111
111
|
description: "Whether to use PKCE for the authorization flow"
|
|
112
112
|
}).default(true).optional()
|
|
113
113
|
}).optional(),
|
|
114
114
|
samlConfig: z.object({
|
|
115
|
-
entryPoint: z.string()
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
entryPoint: z.string({}).meta({
|
|
116
|
+
description: "The entry point of the provider"
|
|
117
|
+
}),
|
|
118
|
+
cert: z.string({}).meta({
|
|
119
|
+
description: "The certificate of the provider"
|
|
120
|
+
}),
|
|
121
|
+
callbackUrl: z.string({}).meta({
|
|
122
|
+
description: "The callback URL of the provider"
|
|
123
|
+
}),
|
|
118
124
|
audience: z.string().optional(),
|
|
119
125
|
idpMetadata: z.object({
|
|
120
126
|
metadata: z.string(),
|
|
@@ -139,30 +145,30 @@ const sso = (options) => {
|
|
|
139
145
|
identifierFormat: z.string().optional(),
|
|
140
146
|
privateKey: z.string().optional(),
|
|
141
147
|
decryptionPvk: z.string().optional(),
|
|
142
|
-
additionalParams: z.record(z.string()).optional()
|
|
148
|
+
additionalParams: z.record(z.string(), z.any()).optional()
|
|
143
149
|
}).optional(),
|
|
144
150
|
mapping: z.object({
|
|
145
|
-
id: z.string({
|
|
151
|
+
id: z.string({}).meta({
|
|
146
152
|
description: "The field in the user info response that contains the id. Defaults to 'sub'"
|
|
147
153
|
}),
|
|
148
|
-
email: z.string({
|
|
154
|
+
email: z.string({}).meta({
|
|
149
155
|
description: "The field in the user info response that contains the email. Defaults to 'email'"
|
|
150
156
|
}),
|
|
151
|
-
emailVerified: z.string({
|
|
157
|
+
emailVerified: z.string({}).meta({
|
|
152
158
|
description: "The field in the user info response that contains whether the email is verified. defaults to 'email_verified'"
|
|
153
159
|
}).optional(),
|
|
154
|
-
name: z.string({
|
|
160
|
+
name: z.string({}).meta({
|
|
155
161
|
description: "The field in the user info response that contains the name. Defaults to 'name'"
|
|
156
162
|
}),
|
|
157
|
-
image: z.string({
|
|
163
|
+
image: z.string({}).meta({
|
|
158
164
|
description: "The field in the user info response that contains the image. Defaults to 'picture'"
|
|
159
165
|
}).optional(),
|
|
160
|
-
extraFields: z.record(z.string()).optional()
|
|
166
|
+
extraFields: z.record(z.string(), z.any()).optional()
|
|
161
167
|
}).optional(),
|
|
162
|
-
organizationId: z.string({
|
|
168
|
+
organizationId: z.string({}).meta({
|
|
163
169
|
description: "If organization plugin is enabled, the organization id to link the provider to"
|
|
164
170
|
}).optional(),
|
|
165
|
-
overrideUserInfo: z.boolean({
|
|
171
|
+
overrideUserInfo: z.boolean({}).meta({
|
|
166
172
|
description: "Override user info with the provider info. Defaults to false"
|
|
167
173
|
}).default(false).optional()
|
|
168
174
|
}),
|
|
@@ -333,6 +339,25 @@ const sso = (options) => {
|
|
|
333
339
|
}
|
|
334
340
|
},
|
|
335
341
|
async (ctx) => {
|
|
342
|
+
const user = ctx.context.session?.user;
|
|
343
|
+
if (!user) {
|
|
344
|
+
throw new APIError("UNAUTHORIZED");
|
|
345
|
+
}
|
|
346
|
+
const limit = typeof options?.providersLimit === "function" ? await options.providersLimit(user) : options?.providersLimit ?? 10;
|
|
347
|
+
if (!limit) {
|
|
348
|
+
throw new APIError("FORBIDDEN", {
|
|
349
|
+
message: "SSO provider registration is disabled"
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const providers = await ctx.context.adapter.findMany({
|
|
353
|
+
model: "ssoProvider",
|
|
354
|
+
where: [{ field: "userId", value: user.id }]
|
|
355
|
+
});
|
|
356
|
+
if (providers.length >= limit) {
|
|
357
|
+
throw new APIError("FORBIDDEN", {
|
|
358
|
+
message: "You have reached the maximum number of SSO providers"
|
|
359
|
+
});
|
|
360
|
+
}
|
|
336
361
|
const body = ctx.body;
|
|
337
362
|
const issuerValidator = z.string().url();
|
|
338
363
|
if (issuerValidator.safeParse(body.issuer).error) {
|
|
@@ -398,31 +423,31 @@ const sso = (options) => {
|
|
|
398
423
|
{
|
|
399
424
|
method: "POST",
|
|
400
425
|
body: z.object({
|
|
401
|
-
email: z.string({
|
|
426
|
+
email: z.string({}).meta({
|
|
402
427
|
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"
|
|
403
428
|
}).optional(),
|
|
404
|
-
organizationSlug: z.string({
|
|
429
|
+
organizationSlug: z.string({}).meta({
|
|
405
430
|
description: "The slug of the organization to sign in with"
|
|
406
431
|
}).optional(),
|
|
407
|
-
providerId: z.string({
|
|
432
|
+
providerId: z.string({}).meta({
|
|
408
433
|
description: "The ID of the provider to sign in with. This can be provided instead of email or issuer"
|
|
409
434
|
}).optional(),
|
|
410
|
-
domain: z.string({
|
|
435
|
+
domain: z.string({}).meta({
|
|
411
436
|
description: "The domain of the provider."
|
|
412
437
|
}).optional(),
|
|
413
|
-
callbackURL: z.string({
|
|
438
|
+
callbackURL: z.string({}).meta({
|
|
414
439
|
description: "The URL to redirect to after login"
|
|
415
440
|
}),
|
|
416
|
-
errorCallbackURL: z.string({
|
|
441
|
+
errorCallbackURL: z.string({}).meta({
|
|
417
442
|
description: "The URL to redirect to after login"
|
|
418
443
|
}).optional(),
|
|
419
|
-
newUserCallbackURL: z.string({
|
|
444
|
+
newUserCallbackURL: z.string({}).meta({
|
|
420
445
|
description: "The URL to redirect to after login if the user is new"
|
|
421
446
|
}).optional(),
|
|
422
|
-
scopes: z.array(z.string(), {
|
|
447
|
+
scopes: z.array(z.string(), {}).meta({
|
|
423
448
|
description: "Scopes to request from the provider."
|
|
424
449
|
}).optional(),
|
|
425
|
-
requestSignUp: z.boolean({
|
|
450
|
+
requestSignUp: z.boolean({}).meta({
|
|
426
451
|
description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider"
|
|
427
452
|
}).optional(),
|
|
428
453
|
providerType: z.enum(["oidc", "saml"]).optional()
|
|
@@ -601,7 +626,9 @@ const sso = (options) => {
|
|
|
601
626
|
});
|
|
602
627
|
}
|
|
603
628
|
return ctx.json({
|
|
604
|
-
url: loginRequest.context
|
|
629
|
+
url: `${loginRequest.context}&RelayState=${encodeURIComponent(
|
|
630
|
+
body.callbackURL
|
|
631
|
+
)}`,
|
|
605
632
|
redirect: true
|
|
606
633
|
});
|
|
607
634
|
}
|
|
@@ -929,11 +956,11 @@ const sso = (options) => {
|
|
|
929
956
|
extract.attributes[value]
|
|
930
957
|
])
|
|
931
958
|
),
|
|
932
|
-
id: attributes[mapping.id || "nameID"],
|
|
933
|
-
email: attributes[mapping.email || "nameID"],
|
|
959
|
+
id: attributes[mapping.id] || attributes["nameID"],
|
|
960
|
+
email: attributes[mapping.email] || attributes["nameID"] || attributes["email"],
|
|
934
961
|
name: [
|
|
935
|
-
attributes[mapping.firstName || "givenName"],
|
|
936
|
-
attributes[mapping.lastName || "surname"]
|
|
962
|
+
attributes[mapping.firstName] || attributes["givenName"],
|
|
963
|
+
attributes[mapping.lastName] || attributes["surname"]
|
|
937
964
|
].filter(Boolean).join(" ") || parsedResponse.extract.attributes?.displayName,
|
|
938
965
|
attributes: parsedResponse.extract.attributes
|
|
939
966
|
};
|
|
@@ -999,10 +1026,9 @@ const sso = (options) => {
|
|
|
999
1026
|
}
|
|
1000
1027
|
let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
|
|
1001
1028
|
await setSessionCookie(ctx, { session, user });
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
});
|
|
1029
|
+
throw ctx.redirect(
|
|
1030
|
+
RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
|
|
1031
|
+
);
|
|
1006
1032
|
}
|
|
1007
1033
|
)
|
|
1008
1034
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.3.0
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"oauth2-mock-server": "^7.2.0",
|
|
48
48
|
"samlify": "^2.10.0",
|
|
49
49
|
"zod": "^3.24.1",
|
|
50
|
-
"better-auth": "^1.3.0
|
|
50
|
+
"better-auth": "^1.3.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/body-parser": "^1.19.6",
|
package/src/index.ts
CHANGED
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
validateAuthorizationCode,
|
|
14
14
|
validateToken,
|
|
15
15
|
} from "better-auth/oauth2";
|
|
16
|
+
|
|
16
17
|
import { createAuthEndpoint } from "better-auth/plugins";
|
|
17
|
-
import
|
|
18
|
+
import * as z from "zod/v4";
|
|
18
19
|
import * as saml from "samlify";
|
|
19
20
|
import type { BindingContext } from "samlify/types/src/entity";
|
|
20
21
|
import { betterFetch, BetterFetchError } from "@better-fetch/fetch";
|
|
@@ -132,6 +133,21 @@ export interface SSOOptions {
|
|
|
132
133
|
* sign-in need to be called with with requestSignUp as true to create new users.
|
|
133
134
|
*/
|
|
134
135
|
disableImplicitSignUp?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Configure the maximum number of SSO providers a user can register.
|
|
138
|
+
* You can also pass a function that returns a number.
|
|
139
|
+
* Set to 0 to disable SSO provider registration.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* providersLimit: async (user) => {
|
|
144
|
+
* const plan = await getUserPlan(user);
|
|
145
|
+
* return plan.name === "pro" ? 10 : 1;
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
* @default 10
|
|
149
|
+
*/
|
|
150
|
+
providersLimit?: number | ((user: User) => Promise<number> | number);
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
export const sso = (options?: SSOOptions) => {
|
|
@@ -192,37 +208,40 @@ export const sso = (options?: SSOOptions) => {
|
|
|
192
208
|
{
|
|
193
209
|
method: "POST",
|
|
194
210
|
body: z.object({
|
|
195
|
-
providerId: z.string({
|
|
211
|
+
providerId: z.string({}).meta({
|
|
196
212
|
description:
|
|
197
213
|
"The ID of the provider. This is used to identify the provider during login and callback",
|
|
198
214
|
}),
|
|
199
|
-
issuer: z.string({
|
|
215
|
+
issuer: z.string({}).meta({
|
|
200
216
|
description: "The issuer of the provider",
|
|
201
217
|
}),
|
|
202
|
-
domain: z.string({
|
|
218
|
+
domain: z.string({}).meta({
|
|
203
219
|
description:
|
|
204
220
|
"The domain of the provider. This is used for email matching",
|
|
205
221
|
}),
|
|
206
222
|
oidcConfig: z
|
|
207
223
|
.object({
|
|
208
|
-
clientId: z.string({
|
|
224
|
+
clientId: z.string({}).meta({
|
|
209
225
|
description: "The client ID",
|
|
210
226
|
}),
|
|
211
|
-
clientSecret: z.string({
|
|
227
|
+
clientSecret: z.string({}).meta({
|
|
212
228
|
description: "The client secret",
|
|
213
229
|
}),
|
|
214
230
|
authorizationEndpoint: z
|
|
215
|
-
.string({
|
|
231
|
+
.string({})
|
|
232
|
+
.meta({
|
|
216
233
|
description: "The authorization endpoint",
|
|
217
234
|
})
|
|
218
235
|
.optional(),
|
|
219
236
|
tokenEndpoint: z
|
|
220
|
-
.string({
|
|
237
|
+
.string({})
|
|
238
|
+
.meta({
|
|
221
239
|
description: "The token endpoint",
|
|
222
240
|
})
|
|
223
241
|
.optional(),
|
|
224
242
|
userInfoEndpoint: z
|
|
225
|
-
.string({
|
|
243
|
+
.string({})
|
|
244
|
+
.meta({
|
|
226
245
|
description: "The user info endpoint",
|
|
227
246
|
})
|
|
228
247
|
.optional(),
|
|
@@ -230,19 +249,22 @@ export const sso = (options?: SSOOptions) => {
|
|
|
230
249
|
.enum(["client_secret_post", "client_secret_basic"])
|
|
231
250
|
.optional(),
|
|
232
251
|
jwksEndpoint: z
|
|
233
|
-
.string({
|
|
252
|
+
.string({})
|
|
253
|
+
.meta({
|
|
234
254
|
description: "The JWKS endpoint",
|
|
235
255
|
})
|
|
236
256
|
.optional(),
|
|
237
257
|
discoveryEndpoint: z.string().optional(),
|
|
238
258
|
scopes: z
|
|
239
|
-
.array(z.string(), {
|
|
259
|
+
.array(z.string(), {})
|
|
260
|
+
.meta({
|
|
240
261
|
description:
|
|
241
262
|
"The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
|
|
242
263
|
})
|
|
243
264
|
.optional(),
|
|
244
265
|
pkce: z
|
|
245
|
-
.boolean({
|
|
266
|
+
.boolean({})
|
|
267
|
+
.meta({
|
|
246
268
|
description:
|
|
247
269
|
"Whether to use PKCE for the authorization flow",
|
|
248
270
|
})
|
|
@@ -252,9 +274,15 @@ export const sso = (options?: SSOOptions) => {
|
|
|
252
274
|
.optional(),
|
|
253
275
|
samlConfig: z
|
|
254
276
|
.object({
|
|
255
|
-
entryPoint: z.string()
|
|
256
|
-
|
|
257
|
-
|
|
277
|
+
entryPoint: z.string({}).meta({
|
|
278
|
+
description: "The entry point of the provider",
|
|
279
|
+
}),
|
|
280
|
+
cert: z.string({}).meta({
|
|
281
|
+
description: "The certificate of the provider",
|
|
282
|
+
}),
|
|
283
|
+
callbackUrl: z.string({}).meta({
|
|
284
|
+
description: "The callback URL of the provider",
|
|
285
|
+
}),
|
|
258
286
|
audience: z.string().optional(),
|
|
259
287
|
idpMetadata: z
|
|
260
288
|
.object({
|
|
@@ -282,46 +310,50 @@ export const sso = (options?: SSOOptions) => {
|
|
|
282
310
|
identifierFormat: z.string().optional(),
|
|
283
311
|
privateKey: z.string().optional(),
|
|
284
312
|
decryptionPvk: z.string().optional(),
|
|
285
|
-
additionalParams: z.record(z.string()).optional(),
|
|
313
|
+
additionalParams: z.record(z.string(), z.any()).optional(),
|
|
286
314
|
})
|
|
287
315
|
.optional(),
|
|
288
316
|
mapping: z
|
|
289
317
|
.object({
|
|
290
|
-
id: z.string({
|
|
318
|
+
id: z.string({}).meta({
|
|
291
319
|
description:
|
|
292
320
|
"The field in the user info response that contains the id. Defaults to 'sub'",
|
|
293
321
|
}),
|
|
294
|
-
email: z.string({
|
|
322
|
+
email: z.string({}).meta({
|
|
295
323
|
description:
|
|
296
324
|
"The field in the user info response that contains the email. Defaults to 'email'",
|
|
297
325
|
}),
|
|
298
326
|
emailVerified: z
|
|
299
|
-
.string({
|
|
327
|
+
.string({})
|
|
328
|
+
.meta({
|
|
300
329
|
description:
|
|
301
330
|
"The field in the user info response that contains whether the email is verified. defaults to 'email_verified'",
|
|
302
331
|
})
|
|
303
332
|
.optional(),
|
|
304
|
-
name: z.string({
|
|
333
|
+
name: z.string({}).meta({
|
|
305
334
|
description:
|
|
306
335
|
"The field in the user info response that contains the name. Defaults to 'name'",
|
|
307
336
|
}),
|
|
308
337
|
image: z
|
|
309
|
-
.string({
|
|
338
|
+
.string({})
|
|
339
|
+
.meta({
|
|
310
340
|
description:
|
|
311
341
|
"The field in the user info response that contains the image. Defaults to 'picture'",
|
|
312
342
|
})
|
|
313
343
|
.optional(),
|
|
314
|
-
extraFields: z.record(z.string()).optional(),
|
|
344
|
+
extraFields: z.record(z.string(), z.any()).optional(),
|
|
315
345
|
})
|
|
316
346
|
.optional(),
|
|
317
347
|
organizationId: z
|
|
318
|
-
.string({
|
|
348
|
+
.string({})
|
|
349
|
+
.meta({
|
|
319
350
|
description:
|
|
320
351
|
"If organization plugin is enabled, the organization id to link the provider to",
|
|
321
352
|
})
|
|
322
353
|
.optional(),
|
|
323
354
|
overrideUserInfo: z
|
|
324
|
-
.boolean({
|
|
355
|
+
.boolean({})
|
|
356
|
+
.meta({
|
|
325
357
|
description:
|
|
326
358
|
"Override user info with the provider info. Defaults to false",
|
|
327
359
|
})
|
|
@@ -509,6 +541,33 @@ export const sso = (options?: SSOOptions) => {
|
|
|
509
541
|
},
|
|
510
542
|
},
|
|
511
543
|
async (ctx) => {
|
|
544
|
+
const user = ctx.context.session?.user;
|
|
545
|
+
if (!user) {
|
|
546
|
+
throw new APIError("UNAUTHORIZED");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const limit =
|
|
550
|
+
typeof options?.providersLimit === "function"
|
|
551
|
+
? await options.providersLimit(user)
|
|
552
|
+
: options?.providersLimit ?? 10;
|
|
553
|
+
|
|
554
|
+
if (!limit) {
|
|
555
|
+
throw new APIError("FORBIDDEN", {
|
|
556
|
+
message: "SSO provider registration is disabled",
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const providers = await ctx.context.adapter.findMany({
|
|
561
|
+
model: "ssoProvider",
|
|
562
|
+
where: [{ field: "userId", value: user.id }],
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
if (providers.length >= limit) {
|
|
566
|
+
throw new APIError("FORBIDDEN", {
|
|
567
|
+
message: "You have reached the maximum number of SSO providers",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
512
571
|
const body = ctx.body;
|
|
513
572
|
const issuerValidator = z.string().url();
|
|
514
573
|
if (issuerValidator.safeParse(body.issuer).error) {
|
|
@@ -589,48 +648,56 @@ export const sso = (options?: SSOOptions) => {
|
|
|
589
648
|
method: "POST",
|
|
590
649
|
body: z.object({
|
|
591
650
|
email: z
|
|
592
|
-
.string({
|
|
651
|
+
.string({})
|
|
652
|
+
.meta({
|
|
593
653
|
description:
|
|
594
654
|
"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",
|
|
595
655
|
})
|
|
596
656
|
.optional(),
|
|
597
657
|
organizationSlug: z
|
|
598
|
-
.string({
|
|
658
|
+
.string({})
|
|
659
|
+
.meta({
|
|
599
660
|
description: "The slug of the organization to sign in with",
|
|
600
661
|
})
|
|
601
662
|
.optional(),
|
|
602
663
|
providerId: z
|
|
603
|
-
.string({
|
|
664
|
+
.string({})
|
|
665
|
+
.meta({
|
|
604
666
|
description:
|
|
605
667
|
"The ID of the provider to sign in with. This can be provided instead of email or issuer",
|
|
606
668
|
})
|
|
607
669
|
.optional(),
|
|
608
670
|
domain: z
|
|
609
|
-
.string({
|
|
671
|
+
.string({})
|
|
672
|
+
.meta({
|
|
610
673
|
description: "The domain of the provider.",
|
|
611
674
|
})
|
|
612
675
|
.optional(),
|
|
613
|
-
callbackURL: z.string({
|
|
676
|
+
callbackURL: z.string({}).meta({
|
|
614
677
|
description: "The URL to redirect to after login",
|
|
615
678
|
}),
|
|
616
679
|
errorCallbackURL: z
|
|
617
|
-
.string({
|
|
680
|
+
.string({})
|
|
681
|
+
.meta({
|
|
618
682
|
description: "The URL to redirect to after login",
|
|
619
683
|
})
|
|
620
684
|
.optional(),
|
|
621
685
|
newUserCallbackURL: z
|
|
622
|
-
.string({
|
|
686
|
+
.string({})
|
|
687
|
+
.meta({
|
|
623
688
|
description:
|
|
624
689
|
"The URL to redirect to after login if the user is new",
|
|
625
690
|
})
|
|
626
691
|
.optional(),
|
|
627
692
|
scopes: z
|
|
628
|
-
.array(z.string(), {
|
|
693
|
+
.array(z.string(), {})
|
|
694
|
+
.meta({
|
|
629
695
|
description: "Scopes to request from the provider.",
|
|
630
696
|
})
|
|
631
697
|
.optional(),
|
|
632
698
|
requestSignUp: z
|
|
633
|
-
.boolean({
|
|
699
|
+
.boolean({})
|
|
700
|
+
.meta({
|
|
634
701
|
description:
|
|
635
702
|
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
|
|
636
703
|
})
|
|
@@ -830,7 +897,9 @@ export const sso = (options?: SSOOptions) => {
|
|
|
830
897
|
});
|
|
831
898
|
}
|
|
832
899
|
return ctx.json({
|
|
833
|
-
url: loginRequest.context
|
|
900
|
+
url: `${loginRequest.context}&RelayState=${encodeURIComponent(
|
|
901
|
+
body.callbackURL,
|
|
902
|
+
)}`,
|
|
834
903
|
redirect: true,
|
|
835
904
|
});
|
|
836
905
|
}
|
|
@@ -1243,12 +1312,15 @@ export const sso = (options?: SSOOptions) => {
|
|
|
1243
1312
|
extract.attributes[value as string],
|
|
1244
1313
|
]),
|
|
1245
1314
|
),
|
|
1246
|
-
id: attributes[mapping.id || "nameID"],
|
|
1247
|
-
email:
|
|
1315
|
+
id: attributes[mapping.id] || attributes["nameID"],
|
|
1316
|
+
email:
|
|
1317
|
+
attributes[mapping.email] ||
|
|
1318
|
+
attributes["nameID"] ||
|
|
1319
|
+
attributes["email"],
|
|
1248
1320
|
name:
|
|
1249
1321
|
[
|
|
1250
|
-
attributes[mapping.firstName || "givenName"],
|
|
1251
|
-
attributes[mapping.lastName || "surname"],
|
|
1322
|
+
attributes[mapping.firstName] || attributes["givenName"],
|
|
1323
|
+
attributes[mapping.lastName] || attributes["surname"],
|
|
1252
1324
|
]
|
|
1253
1325
|
.filter(Boolean)
|
|
1254
1326
|
.join(" ") || parsedResponse.extract.attributes?.displayName,
|
|
@@ -1328,10 +1400,11 @@ export const sso = (options?: SSOOptions) => {
|
|
|
1328
1400
|
let session: Session =
|
|
1329
1401
|
await ctx.context.internalAdapter.createSession(user.id, ctx);
|
|
1330
1402
|
await setSessionCookie(ctx, { session, user });
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1403
|
+
throw ctx.redirect(
|
|
1404
|
+
RelayState ||
|
|
1405
|
+
`${parsedSamlConfig.callbackUrl}` ||
|
|
1406
|
+
`${parsedSamlConfig.issuer}`,
|
|
1407
|
+
);
|
|
1335
1408
|
},
|
|
1336
1409
|
),
|
|
1337
1410
|
},
|
package/src/oidc.test.ts
CHANGED
|
@@ -307,7 +307,108 @@ describe("SSO disable implicit sign in", async () => {
|
|
|
307
307
|
userId: expect.any(String),
|
|
308
308
|
});
|
|
309
309
|
});
|
|
310
|
+
it("should not allow creating a provider if limit is set to 0", async () => {
|
|
311
|
+
const { auth, signInWithTestUser } = await getTestInstanceMemory({
|
|
312
|
+
plugins: [sso({ providersLimit: 0 })],
|
|
313
|
+
});
|
|
314
|
+
const { headers } = await signInWithTestUser();
|
|
315
|
+
await expect(
|
|
316
|
+
auth.api.registerSSOProvider({
|
|
317
|
+
body: {
|
|
318
|
+
issuer: server.issuer.url!,
|
|
319
|
+
domain: "localhost.com",
|
|
320
|
+
oidcConfig: {
|
|
321
|
+
clientId: "test",
|
|
322
|
+
clientSecret: "test",
|
|
323
|
+
},
|
|
324
|
+
providerId: "test",
|
|
325
|
+
},
|
|
326
|
+
headers,
|
|
327
|
+
}),
|
|
328
|
+
).rejects.toMatchObject({
|
|
329
|
+
status: "FORBIDDEN",
|
|
330
|
+
body: { message: "SSO provider registration is disabled" },
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
it("should not allow creating a provider if limit is reached", async () => {
|
|
334
|
+
const { auth, signInWithTestUser } = await getTestInstanceMemory({
|
|
335
|
+
plugins: [sso({ providersLimit: 1 })],
|
|
336
|
+
});
|
|
337
|
+
const { headers } = await signInWithTestUser();
|
|
338
|
+
|
|
339
|
+
await auth.api.registerSSOProvider({
|
|
340
|
+
body: {
|
|
341
|
+
issuer: server.issuer.url!,
|
|
342
|
+
domain: "localhost.com",
|
|
343
|
+
oidcConfig: {
|
|
344
|
+
clientId: "test",
|
|
345
|
+
clientSecret: "test",
|
|
346
|
+
},
|
|
347
|
+
providerId: "test-1",
|
|
348
|
+
},
|
|
349
|
+
headers,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await expect(
|
|
353
|
+
auth.api.registerSSOProvider({
|
|
354
|
+
body: {
|
|
355
|
+
issuer: server.issuer.url!,
|
|
356
|
+
domain: "localhost.com",
|
|
357
|
+
oidcConfig: {
|
|
358
|
+
clientId: "test",
|
|
359
|
+
clientSecret: "test",
|
|
360
|
+
},
|
|
361
|
+
providerId: "test-2",
|
|
362
|
+
},
|
|
363
|
+
headers,
|
|
364
|
+
}),
|
|
365
|
+
).rejects.toMatchObject({
|
|
366
|
+
status: "FORBIDDEN",
|
|
367
|
+
body: {
|
|
368
|
+
message: "You have reached the maximum number of SSO providers",
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should not allow creating a provider if limit from function is reached", async () => {
|
|
374
|
+
const { auth, signInWithTestUser } = await getTestInstanceMemory({
|
|
375
|
+
plugins: [sso({ providersLimit: async () => 1 })],
|
|
376
|
+
});
|
|
377
|
+
const { headers } = await signInWithTestUser();
|
|
310
378
|
|
|
379
|
+
await auth.api.registerSSOProvider({
|
|
380
|
+
body: {
|
|
381
|
+
issuer: server.issuer.url!,
|
|
382
|
+
domain: "localhost.com",
|
|
383
|
+
oidcConfig: {
|
|
384
|
+
clientId: "test",
|
|
385
|
+
clientSecret: "test",
|
|
386
|
+
},
|
|
387
|
+
providerId: "test-1",
|
|
388
|
+
},
|
|
389
|
+
headers,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await expect(
|
|
393
|
+
auth.api.registerSSOProvider({
|
|
394
|
+
body: {
|
|
395
|
+
issuer: server.issuer.url!,
|
|
396
|
+
domain: "localhost.com",
|
|
397
|
+
oidcConfig: {
|
|
398
|
+
clientId: "test",
|
|
399
|
+
clientSecret: "test",
|
|
400
|
+
},
|
|
401
|
+
providerId: "test-2",
|
|
402
|
+
},
|
|
403
|
+
headers,
|
|
404
|
+
}),
|
|
405
|
+
).rejects.toMatchObject({
|
|
406
|
+
status: "FORBIDDEN",
|
|
407
|
+
body: {
|
|
408
|
+
message: "You have reached the maximum number of SSO providers",
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
});
|
|
311
412
|
it("should not create user with SSO provider when sign ups are disabled", async () => {
|
|
312
413
|
const res = await auth.api.signInSSO({
|
|
313
414
|
body: {
|