@hammadj/better-auth-sso 1.5.0-beta.9

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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +116 -0
  2. package/LICENSE.md +20 -0
  3. package/dist/client.d.mts +10 -0
  4. package/dist/client.mjs +15 -0
  5. package/dist/client.mjs.map +1 -0
  6. package/dist/index.d.mts +738 -0
  7. package/dist/index.mjs +2953 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +87 -0
  10. package/src/client.ts +29 -0
  11. package/src/constants.ts +58 -0
  12. package/src/domain-verification.test.ts +551 -0
  13. package/src/index.ts +265 -0
  14. package/src/linking/index.ts +2 -0
  15. package/src/linking/org-assignment.test.ts +325 -0
  16. package/src/linking/org-assignment.ts +176 -0
  17. package/src/linking/types.ts +10 -0
  18. package/src/oidc/discovery.test.ts +1157 -0
  19. package/src/oidc/discovery.ts +494 -0
  20. package/src/oidc/errors.ts +92 -0
  21. package/src/oidc/index.ts +31 -0
  22. package/src/oidc/types.ts +219 -0
  23. package/src/oidc.test.ts +688 -0
  24. package/src/providers.test.ts +1326 -0
  25. package/src/routes/domain-verification.ts +275 -0
  26. package/src/routes/providers.ts +565 -0
  27. package/src/routes/schemas.ts +96 -0
  28. package/src/routes/sso.ts +2750 -0
  29. package/src/saml/algorithms.test.ts +449 -0
  30. package/src/saml/algorithms.ts +338 -0
  31. package/src/saml/assertions.test.ts +239 -0
  32. package/src/saml/assertions.ts +62 -0
  33. package/src/saml/index.ts +13 -0
  34. package/src/saml/parser.ts +56 -0
  35. package/src/saml-state.ts +78 -0
  36. package/src/saml.test.ts +4319 -0
  37. package/src/types.ts +365 -0
  38. package/src/utils.test.ts +103 -0
  39. package/src/utils.ts +81 -0
  40. package/tsconfig.json +14 -0
  41. package/tsdown.config.ts +9 -0
  42. package/vitest.config.ts +3 -0
@@ -0,0 +1,565 @@
1
+ import type { AuthContext } from "better-auth";
2
+ import {
3
+ APIError,
4
+ createAuthEndpoint,
5
+ sessionMiddleware,
6
+ } from "better-auth/api";
7
+ import z from "zod/v4";
8
+ import { DEFAULT_MAX_SAML_METADATA_SIZE } from "../constants";
9
+ import { validateConfigAlgorithms } from "../saml";
10
+ import type { Member, OIDCConfig, SAMLConfig, SSOOptions } from "../types";
11
+ import { maskClientId, parseCertificate, safeJsonParse } from "../utils";
12
+ import { updateSSOProviderBodySchema } from "./schemas";
13
+
14
+ interface SSOProviderRecord {
15
+ id: string;
16
+ providerId: string;
17
+ issuer: string;
18
+ domain: string;
19
+ organizationId?: string | null;
20
+ domainVerified?: boolean;
21
+ userId: string;
22
+ oidcConfig?: string | null;
23
+ samlConfig?: string | null;
24
+ }
25
+
26
+ const ADMIN_ROLES = ["owner", "admin"];
27
+
28
+ async function isOrgAdmin(
29
+ ctx: {
30
+ context: {
31
+ adapter: {
32
+ findOne: <T>(query: {
33
+ model: string;
34
+ where: { field: string; value: string }[];
35
+ }) => Promise<T | null>;
36
+ };
37
+ };
38
+ },
39
+ userId: string,
40
+ organizationId: string,
41
+ ): Promise<boolean> {
42
+ const member = await ctx.context.adapter.findOne<Member>({
43
+ model: "member",
44
+ where: [
45
+ { field: "userId", value: userId },
46
+ { field: "organizationId", value: organizationId },
47
+ ],
48
+ });
49
+ if (!member) return false;
50
+ const roles = member.role.split(",");
51
+ return roles.some((r) => ADMIN_ROLES.includes(r.trim()));
52
+ }
53
+
54
+ async function batchCheckOrgAdmin(
55
+ ctx: {
56
+ context: AuthContext;
57
+ },
58
+ userId: string,
59
+ organizationIds: string[],
60
+ ): Promise<Set<string>> {
61
+ if (organizationIds.length === 0) {
62
+ return new Set();
63
+ }
64
+
65
+ const members = await ctx.context.adapter.findMany<Member>({
66
+ model: "member",
67
+ where: [
68
+ { field: "userId", value: userId },
69
+ { field: "organizationId", value: organizationIds, operator: "in" },
70
+ ],
71
+ });
72
+
73
+ const adminOrgIds = new Set<string>();
74
+ for (const member of members) {
75
+ const roles = member.role.split(",");
76
+ if (roles.some((r: string) => ADMIN_ROLES.includes(r.trim()))) {
77
+ adminOrgIds.add(member.organizationId);
78
+ }
79
+ }
80
+
81
+ return adminOrgIds;
82
+ }
83
+
84
+ function sanitizeProvider(
85
+ provider: {
86
+ providerId: string;
87
+ issuer: string;
88
+ domain: string;
89
+ organizationId?: string | null;
90
+ domainVerified?: boolean;
91
+ oidcConfig?: string | OIDCConfig | null;
92
+ samlConfig?: string | SAMLConfig | null;
93
+ },
94
+ baseURL: string,
95
+ ) {
96
+ let oidcConfig: OIDCConfig | null = null;
97
+ let samlConfig: SAMLConfig | null = null;
98
+
99
+ try {
100
+ oidcConfig = safeJsonParse<OIDCConfig>(provider.oidcConfig as string);
101
+ } catch {
102
+ oidcConfig = null;
103
+ }
104
+
105
+ try {
106
+ samlConfig = safeJsonParse<SAMLConfig>(provider.samlConfig as string);
107
+ } catch {
108
+ samlConfig = null;
109
+ }
110
+
111
+ const type = samlConfig ? "saml" : "oidc";
112
+
113
+ return {
114
+ providerId: provider.providerId,
115
+ type,
116
+ issuer: provider.issuer,
117
+ domain: provider.domain,
118
+ organizationId: provider.organizationId || null,
119
+ domainVerified: provider.domainVerified ?? false,
120
+ oidcConfig: oidcConfig
121
+ ? {
122
+ discoveryEndpoint: oidcConfig.discoveryEndpoint,
123
+ clientIdLastFour: maskClientId(oidcConfig.clientId),
124
+ pkce: oidcConfig.pkce,
125
+ authorizationEndpoint: oidcConfig.authorizationEndpoint,
126
+ tokenEndpoint: oidcConfig.tokenEndpoint,
127
+ userInfoEndpoint: oidcConfig.userInfoEndpoint,
128
+ jwksEndpoint: oidcConfig.jwksEndpoint,
129
+ scopes: oidcConfig.scopes,
130
+ tokenEndpointAuthentication: oidcConfig.tokenEndpointAuthentication,
131
+ }
132
+ : undefined,
133
+ samlConfig: samlConfig
134
+ ? {
135
+ entryPoint: samlConfig.entryPoint,
136
+ callbackUrl: samlConfig.callbackUrl,
137
+ audience: samlConfig.audience,
138
+ wantAssertionsSigned: samlConfig.wantAssertionsSigned,
139
+ authnRequestsSigned: samlConfig.authnRequestsSigned,
140
+ identifierFormat: samlConfig.identifierFormat,
141
+ signatureAlgorithm: samlConfig.signatureAlgorithm,
142
+ digestAlgorithm: samlConfig.digestAlgorithm,
143
+ certificate: (() => {
144
+ try {
145
+ return parseCertificate(samlConfig.cert);
146
+ } catch {
147
+ return { error: "Failed to parse certificate" };
148
+ }
149
+ })(),
150
+ }
151
+ : undefined,
152
+ spMetadataUrl: `${baseURL}/sso/saml2/sp/metadata?providerId=${encodeURIComponent(provider.providerId)}`,
153
+ };
154
+ }
155
+
156
+ export const listSSOProviders = () => {
157
+ return createAuthEndpoint(
158
+ "/sso/providers",
159
+ {
160
+ method: "GET",
161
+ use: [sessionMiddleware],
162
+ metadata: {
163
+ openapi: {
164
+ operationId: "listSSOProviders",
165
+ summary: "List SSO providers",
166
+ description: "Returns a list of SSO providers the user has access to",
167
+ responses: {
168
+ "200": {
169
+ description: "List of SSO providers",
170
+ },
171
+ },
172
+ },
173
+ },
174
+ },
175
+ async (ctx) => {
176
+ const userId = ctx.context.session.user.id;
177
+
178
+ const allProviders =
179
+ await ctx.context.adapter.findMany<SSOProviderRecord>({
180
+ model: "ssoProvider",
181
+ });
182
+
183
+ const userOwnedProviders = allProviders.filter(
184
+ (p) => p.userId === userId && !p.organizationId,
185
+ );
186
+
187
+ const orgProviders = allProviders.filter(
188
+ (p) => p.organizationId !== null && p.organizationId !== undefined,
189
+ );
190
+
191
+ const orgPluginEnabled = ctx.context.hasPlugin("organization");
192
+
193
+ let accessibleProviders: typeof userOwnedProviders = [
194
+ ...userOwnedProviders,
195
+ ];
196
+
197
+ if (orgPluginEnabled && orgProviders.length > 0) {
198
+ const orgIds = [
199
+ ...new Set(
200
+ orgProviders
201
+ .map((p) => p.organizationId)
202
+ .filter((id): id is string => id !== null && id !== undefined),
203
+ ),
204
+ ];
205
+
206
+ const adminOrgIds = await batchCheckOrgAdmin(ctx, userId, orgIds);
207
+
208
+ const orgAccessibleProviders = orgProviders.filter(
209
+ (provider) =>
210
+ provider.organizationId && adminOrgIds.has(provider.organizationId),
211
+ );
212
+
213
+ accessibleProviders = [
214
+ ...accessibleProviders,
215
+ ...orgAccessibleProviders,
216
+ ];
217
+ } else if (!orgPluginEnabled) {
218
+ const userOwnedOrgProviders = orgProviders.filter(
219
+ (p) => p.userId === userId,
220
+ );
221
+ accessibleProviders = [
222
+ ...accessibleProviders,
223
+ ...userOwnedOrgProviders,
224
+ ];
225
+ }
226
+
227
+ const providers = accessibleProviders.map((p) =>
228
+ sanitizeProvider(p, ctx.context.baseURL),
229
+ );
230
+
231
+ return ctx.json({ providers });
232
+ },
233
+ );
234
+ };
235
+
236
+ const getSSOProviderParamsSchema = z.object({
237
+ providerId: z.string(),
238
+ });
239
+
240
+ async function checkProviderAccess(
241
+ ctx: {
242
+ context: AuthContext & {
243
+ session: { user: { id: string } };
244
+ };
245
+ },
246
+ providerId: string,
247
+ ) {
248
+ const userId = ctx.context.session.user.id;
249
+
250
+ const provider = await ctx.context.adapter.findOne<SSOProviderRecord>({
251
+ model: "ssoProvider",
252
+ where: [{ field: "providerId", value: providerId }],
253
+ });
254
+
255
+ if (!provider) {
256
+ throw new APIError("NOT_FOUND", {
257
+ message: "Provider not found",
258
+ });
259
+ }
260
+
261
+ let hasAccess = false;
262
+ if (provider.organizationId) {
263
+ if (ctx.context.hasPlugin("organization")) {
264
+ hasAccess = await isOrgAdmin(ctx, userId, provider.organizationId);
265
+ } else {
266
+ hasAccess = provider.userId === userId;
267
+ }
268
+ } else {
269
+ hasAccess = provider.userId === userId;
270
+ }
271
+
272
+ if (!hasAccess) {
273
+ throw new APIError("FORBIDDEN", {
274
+ message: "You don't have access to this provider",
275
+ });
276
+ }
277
+
278
+ return provider;
279
+ }
280
+
281
+ export const getSSOProvider = () => {
282
+ return createAuthEndpoint(
283
+ "/sso/providers/:providerId",
284
+ {
285
+ method: "GET",
286
+ use: [sessionMiddleware],
287
+ params: getSSOProviderParamsSchema,
288
+ metadata: {
289
+ openapi: {
290
+ operationId: "getSSOProvider",
291
+ summary: "Get SSO provider details",
292
+ description: "Returns sanitized details for a specific SSO provider",
293
+ responses: {
294
+ "200": {
295
+ description: "SSO provider details",
296
+ },
297
+ "404": {
298
+ description: "Provider not found",
299
+ },
300
+ "403": {
301
+ description: "Access denied",
302
+ },
303
+ },
304
+ },
305
+ },
306
+ },
307
+ async (ctx) => {
308
+ const { providerId } = ctx.params;
309
+
310
+ const provider = await checkProviderAccess(ctx, providerId);
311
+
312
+ return ctx.json(sanitizeProvider(provider, ctx.context.baseURL));
313
+ },
314
+ );
315
+ };
316
+
317
+ function parseAndValidateConfig<T>(
318
+ configString: string | null | undefined,
319
+ configType: "SAML" | "OIDC",
320
+ ): T {
321
+ let config: T | null = null;
322
+ try {
323
+ config = safeJsonParse<T>(configString as string);
324
+ } catch {
325
+ config = null;
326
+ }
327
+ if (!config) {
328
+ throw new APIError("BAD_REQUEST", {
329
+ message: `Cannot update ${configType} config for a provider that doesn't have ${configType} configured`,
330
+ });
331
+ }
332
+ return config;
333
+ }
334
+
335
+ function mergeSAMLConfig(
336
+ current: SAMLConfig,
337
+ updates: Partial<SAMLConfig>,
338
+ issuer: string,
339
+ ): SAMLConfig {
340
+ return {
341
+ ...current,
342
+ ...updates,
343
+ issuer,
344
+ entryPoint: updates.entryPoint ?? current.entryPoint,
345
+ cert: updates.cert ?? current.cert,
346
+ callbackUrl: updates.callbackUrl ?? current.callbackUrl,
347
+ spMetadata: updates.spMetadata ?? current.spMetadata,
348
+ idpMetadata: updates.idpMetadata ?? current.idpMetadata,
349
+ mapping: updates.mapping ?? current.mapping,
350
+ audience: updates.audience ?? current.audience,
351
+ wantAssertionsSigned:
352
+ updates.wantAssertionsSigned ?? current.wantAssertionsSigned,
353
+ authnRequestsSigned:
354
+ updates.authnRequestsSigned ?? current.authnRequestsSigned,
355
+ identifierFormat: updates.identifierFormat ?? current.identifierFormat,
356
+ signatureAlgorithm:
357
+ updates.signatureAlgorithm ?? current.signatureAlgorithm,
358
+ digestAlgorithm: updates.digestAlgorithm ?? current.digestAlgorithm,
359
+ };
360
+ }
361
+
362
+ function mergeOIDCConfig(
363
+ current: OIDCConfig,
364
+ updates: Partial<OIDCConfig>,
365
+ issuer: string,
366
+ ): OIDCConfig {
367
+ return {
368
+ ...current,
369
+ ...updates,
370
+ issuer,
371
+ pkce: updates.pkce ?? current.pkce ?? true,
372
+ clientId: updates.clientId ?? current.clientId,
373
+ clientSecret: updates.clientSecret ?? current.clientSecret,
374
+ discoveryEndpoint: updates.discoveryEndpoint ?? current.discoveryEndpoint,
375
+ mapping: updates.mapping ?? current.mapping,
376
+ scopes: updates.scopes ?? current.scopes,
377
+ authorizationEndpoint:
378
+ updates.authorizationEndpoint ?? current.authorizationEndpoint,
379
+ tokenEndpoint: updates.tokenEndpoint ?? current.tokenEndpoint,
380
+ userInfoEndpoint: updates.userInfoEndpoint ?? current.userInfoEndpoint,
381
+ jwksEndpoint: updates.jwksEndpoint ?? current.jwksEndpoint,
382
+ tokenEndpointAuthentication:
383
+ updates.tokenEndpointAuthentication ??
384
+ current.tokenEndpointAuthentication,
385
+ };
386
+ }
387
+
388
+ export const updateSSOProvider = (options: SSOOptions) => {
389
+ return createAuthEndpoint(
390
+ "/sso/providers/:providerId",
391
+ {
392
+ method: "PATCH",
393
+ use: [sessionMiddleware],
394
+ params: getSSOProviderParamsSchema,
395
+ body: updateSSOProviderBodySchema,
396
+ metadata: {
397
+ openapi: {
398
+ operationId: "updateSSOProvider",
399
+ summary: "Update SSO provider",
400
+ description:
401
+ "Partially update an SSO provider. Only provided fields are updated. If domain changes, domainVerified is reset to false.",
402
+ responses: {
403
+ "200": {
404
+ description: "SSO provider updated successfully",
405
+ },
406
+ "404": {
407
+ description: "Provider not found",
408
+ },
409
+ "403": {
410
+ description: "Access denied",
411
+ },
412
+ },
413
+ },
414
+ },
415
+ },
416
+ async (ctx) => {
417
+ const { providerId } = ctx.params;
418
+ const body = ctx.body;
419
+
420
+ const { issuer, domain, samlConfig, oidcConfig } = body;
421
+ if (!issuer && !domain && !samlConfig && !oidcConfig) {
422
+ throw new APIError("BAD_REQUEST", {
423
+ message: "No fields provided for update",
424
+ });
425
+ }
426
+
427
+ const existingProvider = await checkProviderAccess(ctx, providerId);
428
+
429
+ const updateData: Partial<SSOProviderRecord> = {};
430
+
431
+ if (body.issuer !== undefined) {
432
+ updateData.issuer = body.issuer;
433
+ }
434
+
435
+ if (body.domain !== undefined) {
436
+ updateData.domain = body.domain;
437
+ if (body.domain !== existingProvider.domain) {
438
+ updateData.domainVerified = false;
439
+ }
440
+ }
441
+
442
+ if (body.samlConfig) {
443
+ if (body.samlConfig.idpMetadata?.metadata) {
444
+ const maxMetadataSize =
445
+ options?.saml?.maxMetadataSize ?? DEFAULT_MAX_SAML_METADATA_SIZE;
446
+ if (
447
+ new TextEncoder().encode(body.samlConfig.idpMetadata.metadata)
448
+ .length > maxMetadataSize
449
+ ) {
450
+ throw new APIError("BAD_REQUEST", {
451
+ message: `IdP metadata exceeds maximum allowed size (${maxMetadataSize} bytes)`,
452
+ });
453
+ }
454
+ }
455
+
456
+ if (
457
+ body.samlConfig.signatureAlgorithm !== undefined ||
458
+ body.samlConfig.digestAlgorithm !== undefined
459
+ ) {
460
+ validateConfigAlgorithms(
461
+ {
462
+ signatureAlgorithm: body.samlConfig.signatureAlgorithm,
463
+ digestAlgorithm: body.samlConfig.digestAlgorithm,
464
+ },
465
+ options?.saml?.algorithms,
466
+ );
467
+ }
468
+
469
+ const currentSamlConfig = parseAndValidateConfig<SAMLConfig>(
470
+ existingProvider.samlConfig,
471
+ "SAML",
472
+ );
473
+
474
+ const updatedSamlConfig = mergeSAMLConfig(
475
+ currentSamlConfig,
476
+ body.samlConfig,
477
+ updateData.issuer ||
478
+ currentSamlConfig.issuer ||
479
+ existingProvider.issuer,
480
+ );
481
+
482
+ updateData.samlConfig = JSON.stringify(updatedSamlConfig);
483
+ }
484
+
485
+ if (body.oidcConfig) {
486
+ const currentOidcConfig = parseAndValidateConfig<OIDCConfig>(
487
+ existingProvider.oidcConfig,
488
+ "OIDC",
489
+ );
490
+
491
+ const updatedOidcConfig = mergeOIDCConfig(
492
+ currentOidcConfig,
493
+ body.oidcConfig,
494
+ updateData.issuer ||
495
+ currentOidcConfig.issuer ||
496
+ existingProvider.issuer,
497
+ );
498
+
499
+ updateData.oidcConfig = JSON.stringify(updatedOidcConfig);
500
+ }
501
+
502
+ await ctx.context.adapter.update({
503
+ model: "ssoProvider",
504
+ where: [{ field: "providerId", value: providerId }],
505
+ update: updateData,
506
+ });
507
+
508
+ const fullProvider = await ctx.context.adapter.findOne<SSOProviderRecord>(
509
+ {
510
+ model: "ssoProvider",
511
+ where: [{ field: "providerId", value: providerId }],
512
+ },
513
+ );
514
+
515
+ if (!fullProvider) {
516
+ throw new APIError("NOT_FOUND", {
517
+ message: "Provider not found after update",
518
+ });
519
+ }
520
+
521
+ return ctx.json(sanitizeProvider(fullProvider, ctx.context.baseURL));
522
+ },
523
+ );
524
+ };
525
+
526
+ export const deleteSSOProvider = () => {
527
+ return createAuthEndpoint(
528
+ "/sso/providers/:providerId",
529
+ {
530
+ method: "DELETE",
531
+ use: [sessionMiddleware],
532
+ params: getSSOProviderParamsSchema,
533
+ metadata: {
534
+ openapi: {
535
+ operationId: "deleteSSOProvider",
536
+ summary: "Delete SSO provider",
537
+ description: "Deletes an SSO provider",
538
+ responses: {
539
+ "200": {
540
+ description: "SSO provider deleted successfully",
541
+ },
542
+ "404": {
543
+ description: "Provider not found",
544
+ },
545
+ "403": {
546
+ description: "Access denied",
547
+ },
548
+ },
549
+ },
550
+ },
551
+ },
552
+ async (ctx) => {
553
+ const { providerId } = ctx.params;
554
+
555
+ await checkProviderAccess(ctx, providerId);
556
+
557
+ await ctx.context.adapter.delete({
558
+ model: "ssoProvider",
559
+ where: [{ field: "providerId", value: providerId }],
560
+ });
561
+
562
+ return ctx.json({ success: true });
563
+ },
564
+ );
565
+ };
@@ -0,0 +1,96 @@
1
+ import z from "zod/v4";
2
+
3
+ const oidcMappingSchema = z
4
+ .object({
5
+ id: z.string().optional(),
6
+ email: z.string().optional(),
7
+ emailVerified: z.string().optional(),
8
+ name: z.string().optional(),
9
+ image: z.string().optional(),
10
+ extraFields: z.record(z.string(), z.any()).optional(),
11
+ })
12
+ .optional();
13
+
14
+ const samlMappingSchema = z
15
+ .object({
16
+ id: z.string().optional(),
17
+ email: z.string().optional(),
18
+ emailVerified: z.string().optional(),
19
+ name: z.string().optional(),
20
+ firstName: z.string().optional(),
21
+ lastName: z.string().optional(),
22
+ extraFields: z.record(z.string(), z.any()).optional(),
23
+ })
24
+ .optional();
25
+
26
+ const oidcConfigSchema = z.object({
27
+ clientId: z.string().optional(),
28
+ clientSecret: z.string().optional(),
29
+ authorizationEndpoint: z.string().url().optional(),
30
+ tokenEndpoint: z.string().url().optional(),
31
+ userInfoEndpoint: z.string().url().optional(),
32
+ tokenEndpointAuthentication: z
33
+ .enum(["client_secret_post", "client_secret_basic"])
34
+ .optional(),
35
+ jwksEndpoint: z.string().url().optional(),
36
+ discoveryEndpoint: z.string().url().optional(),
37
+ scopes: z.array(z.string()).optional(),
38
+ pkce: z.boolean().optional(),
39
+ overrideUserInfo: z.boolean().optional(),
40
+ mapping: oidcMappingSchema,
41
+ });
42
+
43
+ const samlConfigSchema = z.object({
44
+ entryPoint: z.string().url().optional(),
45
+ cert: z.string().optional(),
46
+ callbackUrl: z.string().url().optional(),
47
+ audience: z.string().optional(),
48
+ idpMetadata: z
49
+ .object({
50
+ metadata: z.string().optional(),
51
+ entityID: z.string().optional(),
52
+ cert: z.string().optional(),
53
+ privateKey: z.string().optional(),
54
+ privateKeyPass: z.string().optional(),
55
+ isAssertionEncrypted: z.boolean().optional(),
56
+ encPrivateKey: z.string().optional(),
57
+ encPrivateKeyPass: z.string().optional(),
58
+ singleSignOnService: z
59
+ .array(
60
+ z.object({
61
+ Binding: z.string(),
62
+ Location: z.string().url(),
63
+ }),
64
+ )
65
+ .optional(),
66
+ })
67
+ .optional(),
68
+ spMetadata: z
69
+ .object({
70
+ metadata: z.string().optional(),
71
+ entityID: z.string().optional(),
72
+ binding: z.string().optional(),
73
+ privateKey: z.string().optional(),
74
+ privateKeyPass: z.string().optional(),
75
+ isAssertionEncrypted: z.boolean().optional(),
76
+ encPrivateKey: z.string().optional(),
77
+ encPrivateKeyPass: z.string().optional(),
78
+ })
79
+ .optional(),
80
+ wantAssertionsSigned: z.boolean().optional(),
81
+ authnRequestsSigned: z.boolean().optional(),
82
+ signatureAlgorithm: z.string().optional(),
83
+ digestAlgorithm: z.string().optional(),
84
+ identifierFormat: z.string().optional(),
85
+ privateKey: z.string().optional(),
86
+ decryptionPvk: z.string().optional(),
87
+ additionalParams: z.record(z.string(), z.any()).optional(),
88
+ mapping: samlMappingSchema,
89
+ });
90
+
91
+ export const updateSSOProviderBodySchema = z.object({
92
+ issuer: z.string().url().optional(),
93
+ domain: z.string().optional(),
94
+ oidcConfig: oidcConfigSchema.optional(),
95
+ samlConfig: samlConfigSchema.optional(),
96
+ });