@better-auth/sso 1.3.0-beta.9 → 1.3.1-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/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/index.cjs +109 -82
- package/dist/index.d.cts +52 -308
- package/dist/index.d.mts +52 -308
- package/dist/index.d.ts +52 -308
- package/dist/index.mjs +66 -40
- package/package.json +2 -2
- package/src/index.ts +112 -40
- package/src/oidc.test.ts +101 -0
- package/src/saml.test.ts +208 -66
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,9 +1026,8 @@ 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
|
-
console.log("RelayState: ", RelayState);
|
|
1003
1029
|
throw ctx.redirect(
|
|
1004
|
-
RelayState || `${parsedSamlConfig.issuer}
|
|
1030
|
+
RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
|
|
1005
1031
|
);
|
|
1006
1032
|
}
|
|
1007
1033
|
)
|
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.
|
|
4
|
+
"version": "1.3.1-beta.1",
|
|
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.
|
|
50
|
+
"better-auth": "^1.3.1-beta.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/body-parser": "^1.19.6",
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "better-auth/oauth2";
|
|
16
16
|
|
|
17
17
|
import { createAuthEndpoint } from "better-auth/plugins";
|
|
18
|
-
import
|
|
18
|
+
import * as z from "zod/v4";
|
|
19
19
|
import * as saml from "samlify";
|
|
20
20
|
import type { BindingContext } from "samlify/types/src/entity";
|
|
21
21
|
import { betterFetch, BetterFetchError } from "@better-fetch/fetch";
|
|
@@ -133,6 +133,21 @@ export interface SSOOptions {
|
|
|
133
133
|
* sign-in need to be called with with requestSignUp as true to create new users.
|
|
134
134
|
*/
|
|
135
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);
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
export const sso = (options?: SSOOptions) => {
|
|
@@ -193,37 +208,40 @@ export const sso = (options?: SSOOptions) => {
|
|
|
193
208
|
{
|
|
194
209
|
method: "POST",
|
|
195
210
|
body: z.object({
|
|
196
|
-
providerId: z.string({
|
|
211
|
+
providerId: z.string({}).meta({
|
|
197
212
|
description:
|
|
198
213
|
"The ID of the provider. This is used to identify the provider during login and callback",
|
|
199
214
|
}),
|
|
200
|
-
issuer: z.string({
|
|
215
|
+
issuer: z.string({}).meta({
|
|
201
216
|
description: "The issuer of the provider",
|
|
202
217
|
}),
|
|
203
|
-
domain: z.string({
|
|
218
|
+
domain: z.string({}).meta({
|
|
204
219
|
description:
|
|
205
220
|
"The domain of the provider. This is used for email matching",
|
|
206
221
|
}),
|
|
207
222
|
oidcConfig: z
|
|
208
223
|
.object({
|
|
209
|
-
clientId: z.string({
|
|
224
|
+
clientId: z.string({}).meta({
|
|
210
225
|
description: "The client ID",
|
|
211
226
|
}),
|
|
212
|
-
clientSecret: z.string({
|
|
227
|
+
clientSecret: z.string({}).meta({
|
|
213
228
|
description: "The client secret",
|
|
214
229
|
}),
|
|
215
230
|
authorizationEndpoint: z
|
|
216
|
-
.string({
|
|
231
|
+
.string({})
|
|
232
|
+
.meta({
|
|
217
233
|
description: "The authorization endpoint",
|
|
218
234
|
})
|
|
219
235
|
.optional(),
|
|
220
236
|
tokenEndpoint: z
|
|
221
|
-
.string({
|
|
237
|
+
.string({})
|
|
238
|
+
.meta({
|
|
222
239
|
description: "The token endpoint",
|
|
223
240
|
})
|
|
224
241
|
.optional(),
|
|
225
242
|
userInfoEndpoint: z
|
|
226
|
-
.string({
|
|
243
|
+
.string({})
|
|
244
|
+
.meta({
|
|
227
245
|
description: "The user info endpoint",
|
|
228
246
|
})
|
|
229
247
|
.optional(),
|
|
@@ -231,19 +249,22 @@ export const sso = (options?: SSOOptions) => {
|
|
|
231
249
|
.enum(["client_secret_post", "client_secret_basic"])
|
|
232
250
|
.optional(),
|
|
233
251
|
jwksEndpoint: z
|
|
234
|
-
.string({
|
|
252
|
+
.string({})
|
|
253
|
+
.meta({
|
|
235
254
|
description: "The JWKS endpoint",
|
|
236
255
|
})
|
|
237
256
|
.optional(),
|
|
238
257
|
discoveryEndpoint: z.string().optional(),
|
|
239
258
|
scopes: z
|
|
240
|
-
.array(z.string(), {
|
|
259
|
+
.array(z.string(), {})
|
|
260
|
+
.meta({
|
|
241
261
|
description:
|
|
242
262
|
"The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
|
|
243
263
|
})
|
|
244
264
|
.optional(),
|
|
245
265
|
pkce: z
|
|
246
|
-
.boolean({
|
|
266
|
+
.boolean({})
|
|
267
|
+
.meta({
|
|
247
268
|
description:
|
|
248
269
|
"Whether to use PKCE for the authorization flow",
|
|
249
270
|
})
|
|
@@ -253,9 +274,15 @@ export const sso = (options?: SSOOptions) => {
|
|
|
253
274
|
.optional(),
|
|
254
275
|
samlConfig: z
|
|
255
276
|
.object({
|
|
256
|
-
entryPoint: z.string()
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
}),
|
|
259
286
|
audience: z.string().optional(),
|
|
260
287
|
idpMetadata: z
|
|
261
288
|
.object({
|
|
@@ -283,46 +310,50 @@ export const sso = (options?: SSOOptions) => {
|
|
|
283
310
|
identifierFormat: z.string().optional(),
|
|
284
311
|
privateKey: z.string().optional(),
|
|
285
312
|
decryptionPvk: z.string().optional(),
|
|
286
|
-
additionalParams: z.record(z.string()).optional(),
|
|
313
|
+
additionalParams: z.record(z.string(), z.any()).optional(),
|
|
287
314
|
})
|
|
288
315
|
.optional(),
|
|
289
316
|
mapping: z
|
|
290
317
|
.object({
|
|
291
|
-
id: z.string({
|
|
318
|
+
id: z.string({}).meta({
|
|
292
319
|
description:
|
|
293
320
|
"The field in the user info response that contains the id. Defaults to 'sub'",
|
|
294
321
|
}),
|
|
295
|
-
email: z.string({
|
|
322
|
+
email: z.string({}).meta({
|
|
296
323
|
description:
|
|
297
324
|
"The field in the user info response that contains the email. Defaults to 'email'",
|
|
298
325
|
}),
|
|
299
326
|
emailVerified: z
|
|
300
|
-
.string({
|
|
327
|
+
.string({})
|
|
328
|
+
.meta({
|
|
301
329
|
description:
|
|
302
330
|
"The field in the user info response that contains whether the email is verified. defaults to 'email_verified'",
|
|
303
331
|
})
|
|
304
332
|
.optional(),
|
|
305
|
-
name: z.string({
|
|
333
|
+
name: z.string({}).meta({
|
|
306
334
|
description:
|
|
307
335
|
"The field in the user info response that contains the name. Defaults to 'name'",
|
|
308
336
|
}),
|
|
309
337
|
image: z
|
|
310
|
-
.string({
|
|
338
|
+
.string({})
|
|
339
|
+
.meta({
|
|
311
340
|
description:
|
|
312
341
|
"The field in the user info response that contains the image. Defaults to 'picture'",
|
|
313
342
|
})
|
|
314
343
|
.optional(),
|
|
315
|
-
extraFields: z.record(z.string()).optional(),
|
|
344
|
+
extraFields: z.record(z.string(), z.any()).optional(),
|
|
316
345
|
})
|
|
317
346
|
.optional(),
|
|
318
347
|
organizationId: z
|
|
319
|
-
.string({
|
|
348
|
+
.string({})
|
|
349
|
+
.meta({
|
|
320
350
|
description:
|
|
321
351
|
"If organization plugin is enabled, the organization id to link the provider to",
|
|
322
352
|
})
|
|
323
353
|
.optional(),
|
|
324
354
|
overrideUserInfo: z
|
|
325
|
-
.boolean({
|
|
355
|
+
.boolean({})
|
|
356
|
+
.meta({
|
|
326
357
|
description:
|
|
327
358
|
"Override user info with the provider info. Defaults to false",
|
|
328
359
|
})
|
|
@@ -510,6 +541,33 @@ export const sso = (options?: SSOOptions) => {
|
|
|
510
541
|
},
|
|
511
542
|
},
|
|
512
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
|
+
|
|
513
571
|
const body = ctx.body;
|
|
514
572
|
const issuerValidator = z.string().url();
|
|
515
573
|
if (issuerValidator.safeParse(body.issuer).error) {
|
|
@@ -590,48 +648,56 @@ export const sso = (options?: SSOOptions) => {
|
|
|
590
648
|
method: "POST",
|
|
591
649
|
body: z.object({
|
|
592
650
|
email: z
|
|
593
|
-
.string({
|
|
651
|
+
.string({})
|
|
652
|
+
.meta({
|
|
594
653
|
description:
|
|
595
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",
|
|
596
655
|
})
|
|
597
656
|
.optional(),
|
|
598
657
|
organizationSlug: z
|
|
599
|
-
.string({
|
|
658
|
+
.string({})
|
|
659
|
+
.meta({
|
|
600
660
|
description: "The slug of the organization to sign in with",
|
|
601
661
|
})
|
|
602
662
|
.optional(),
|
|
603
663
|
providerId: z
|
|
604
|
-
.string({
|
|
664
|
+
.string({})
|
|
665
|
+
.meta({
|
|
605
666
|
description:
|
|
606
667
|
"The ID of the provider to sign in with. This can be provided instead of email or issuer",
|
|
607
668
|
})
|
|
608
669
|
.optional(),
|
|
609
670
|
domain: z
|
|
610
|
-
.string({
|
|
671
|
+
.string({})
|
|
672
|
+
.meta({
|
|
611
673
|
description: "The domain of the provider.",
|
|
612
674
|
})
|
|
613
675
|
.optional(),
|
|
614
|
-
callbackURL: z.string({
|
|
676
|
+
callbackURL: z.string({}).meta({
|
|
615
677
|
description: "The URL to redirect to after login",
|
|
616
678
|
}),
|
|
617
679
|
errorCallbackURL: z
|
|
618
|
-
.string({
|
|
680
|
+
.string({})
|
|
681
|
+
.meta({
|
|
619
682
|
description: "The URL to redirect to after login",
|
|
620
683
|
})
|
|
621
684
|
.optional(),
|
|
622
685
|
newUserCallbackURL: z
|
|
623
|
-
.string({
|
|
686
|
+
.string({})
|
|
687
|
+
.meta({
|
|
624
688
|
description:
|
|
625
689
|
"The URL to redirect to after login if the user is new",
|
|
626
690
|
})
|
|
627
691
|
.optional(),
|
|
628
692
|
scopes: z
|
|
629
|
-
.array(z.string(), {
|
|
693
|
+
.array(z.string(), {})
|
|
694
|
+
.meta({
|
|
630
695
|
description: "Scopes to request from the provider.",
|
|
631
696
|
})
|
|
632
697
|
.optional(),
|
|
633
698
|
requestSignUp: z
|
|
634
|
-
.boolean({
|
|
699
|
+
.boolean({})
|
|
700
|
+
.meta({
|
|
635
701
|
description:
|
|
636
702
|
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
|
|
637
703
|
})
|
|
@@ -831,7 +897,9 @@ export const sso = (options?: SSOOptions) => {
|
|
|
831
897
|
});
|
|
832
898
|
}
|
|
833
899
|
return ctx.json({
|
|
834
|
-
url: loginRequest.context
|
|
900
|
+
url: `${loginRequest.context}&RelayState=${encodeURIComponent(
|
|
901
|
+
body.callbackURL,
|
|
902
|
+
)}`,
|
|
835
903
|
redirect: true,
|
|
836
904
|
});
|
|
837
905
|
}
|
|
@@ -1244,12 +1312,15 @@ export const sso = (options?: SSOOptions) => {
|
|
|
1244
1312
|
extract.attributes[value as string],
|
|
1245
1313
|
]),
|
|
1246
1314
|
),
|
|
1247
|
-
id: attributes[mapping.id || "nameID"],
|
|
1248
|
-
email:
|
|
1315
|
+
id: attributes[mapping.id] || attributes["nameID"],
|
|
1316
|
+
email:
|
|
1317
|
+
attributes[mapping.email] ||
|
|
1318
|
+
attributes["nameID"] ||
|
|
1319
|
+
attributes["email"],
|
|
1249
1320
|
name:
|
|
1250
1321
|
[
|
|
1251
|
-
attributes[mapping.firstName || "givenName"],
|
|
1252
|
-
attributes[mapping.lastName || "surname"],
|
|
1322
|
+
attributes[mapping.firstName] || attributes["givenName"],
|
|
1323
|
+
attributes[mapping.lastName] || attributes["surname"],
|
|
1253
1324
|
]
|
|
1254
1325
|
.filter(Boolean)
|
|
1255
1326
|
.join(" ") || parsedResponse.extract.attributes?.displayName,
|
|
@@ -1329,9 +1400,10 @@ export const sso = (options?: SSOOptions) => {
|
|
|
1329
1400
|
let session: Session =
|
|
1330
1401
|
await ctx.context.internalAdapter.createSession(user.id, ctx);
|
|
1331
1402
|
await setSessionCookie(ctx, { session, user });
|
|
1332
|
-
console.log("RelayState: ", RelayState);
|
|
1333
1403
|
throw ctx.redirect(
|
|
1334
|
-
RelayState ||
|
|
1404
|
+
RelayState ||
|
|
1405
|
+
`${parsedSamlConfig.callbackUrl}` ||
|
|
1406
|
+
`${parsedSamlConfig.issuer}`,
|
|
1335
1407
|
);
|
|
1336
1408
|
},
|
|
1337
1409
|
),
|
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: {
|