diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 4221eadcb..8ff12069a 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -52,6 +52,7 @@ import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-acces import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; +import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; @@ -162,6 +163,7 @@ declare module "fastify" { identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory; + identityJwtAuth: TIdentityJwtAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index f5c44ff79..ff3268ab5 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -98,6 +98,8 @@ import { TIdentityGcpAuths, TIdentityGcpAuthsInsert, TIdentityGcpAuthsUpdate, + TIdentityJwtAuths, + TIdentityJwtAuthsUpdate, TIdentityKubernetesAuths, TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsUpdate, @@ -590,6 +592,11 @@ declare module "knex/types/tables" { TIdentityOidcAuthsInsert, TIdentityOidcAuthsUpdate >; + [TableName.IdentityJwtAuth]: KnexOriginal.CompositeTableType< + TIdentityJwtAuths, + TIdentityJwtAuthsInsert, + TIdentityJwtAuthsUpdate + >; [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, diff --git a/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts b/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts new file mode 100644 index 000000000..2e7ac4b63 --- /dev/null +++ b/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts @@ -0,0 +1,34 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityJwtAuth))) { + await knex.schema.createTable(TableName.IdentityJwtAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable(); + t.jsonb("accessTokenTrustedIps").notNullable(); + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.string("configurationType").notNullable(); + t.string("jwksUrl"); + t.binary("encryptedJwksCaCert"); + t.binary("encryptedPublicKeys"); + t.string("boundIssuer"); + t.string("boundAudiences"); + t.jsonb("boundClaims"); + t.string("boundSubject"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.IdentityJwtAuth); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityJwtAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityJwtAuth); +} diff --git a/backend/src/db/schemas/identity-jwt-auths.ts b/backend/src/db/schemas/identity-jwt-auths.ts new file mode 100644 index 000000000..a67fa186e --- /dev/null +++ b/backend/src/db/schemas/identity-jwt-auths.ts @@ -0,0 +1,33 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityJwtAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + identityId: z.string().uuid(), + configurationType: z.string(), + jwksUrl: z.string().nullable().optional(), + encryptedJwksCaCert: zodBuffer.nullable().optional(), + encryptedPublicKeys: zodBuffer.nullable().optional(), + boundIssuer: z.string().nullable().optional(), + boundAudiences: z.string().nullable().optional(), + boundClaims: z.unknown().nullable().optional(), + boundSubject: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityJwtAuths = z.infer; +export type TIdentityJwtAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityJwtAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 74741a8ff..bd26610d9 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -30,6 +30,7 @@ export * from "./identity-access-tokens"; export * from "./identity-aws-auths"; export * from "./identity-azure-auths"; export * from "./identity-gcp-auths"; +export * from "./identity-jwt-auths"; export * from "./identity-kubernetes-auths"; export * from "./identity-metadata"; export * from "./identity-oidc-auths"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 171931f7e..5ec686140 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -68,6 +68,7 @@ export enum TableName { IdentityUaClientSecret = "identity_ua_client_secrets", IdentityAwsAuth = "identity_aws_auths", IdentityOidcAuth = "identity_oidc_auths", + IdentityJwtAuth = "identity_jwt_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", @@ -196,5 +197,6 @@ export enum IdentityAuthMethod { GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", - OIDC_AUTH = "oidc-auth" + OIDC_AUTH = "oidc-auth", + JWT_AUTH = "jwt-auth" } diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 99822da29..1debf8d60 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -349,6 +349,30 @@ export const OIDC_AUTH = { } } as const; +export const JWT_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + caCert: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.", + configurationType: "The configuration for validating JWTs. Must be one of: 'jwks', 'static'", + jwksUrl: + "The URL of the JWKS endpoint. Required if configurationType is 'jwks'. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.", + jwksCaCert: "The PEM-encoded CA certificate for validating the TLS connection to the JWKS endpoint.", + publicKeys: + "A list of PEM-encoded public keys used to verify JWT signatures. Required if configurationType is 'static'. Each key must be in RSA or ECDSA format and properly PEM-encoded with BEGIN/END markers.", + boundIssuer: "The unique identifier of the identity provider issuing the JWT.", + boundAudiences: "The list of intended recipients.", + boundClaims: "The attributes that should be present in the JWT for it to be valid.", + boundSubject: "The expected principal that is the subject of the JWT.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The lifetime for an access token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." + } +} as const; + export const ORGANIZATIONS = { LIST_USER_MEMBERSHIPS: { organizationId: "The ID of the organization to get memberships from." diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4f07579bd..f8f5550fe 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -121,6 +121,8 @@ import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/i import { identityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal"; import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; +import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-dal"; +import { identityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service"; import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal"; import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; @@ -298,6 +300,7 @@ export const registerRoutes = async ( const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityOidcAuthDAL = identityOidcAuthDALFactory(db); + const identityJwtAuthDAL = identityJwtAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(auditLogDb ?? db); @@ -1180,6 +1183,15 @@ export const registerRoutes = async ( orgBotDAL }); + const identityJwtAuthService = identityJwtAuthServiceFactory({ + identityJwtAuthDAL, + permissionService, + identityAccessTokenDAL, + identityOrgMembershipDAL, + licenseService, + kmsService + }); + const dynamicSecretProviders = buildDynamicSecretProviders(); const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ queueService, @@ -1342,6 +1354,7 @@ export const registerRoutes = async ( identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, identityOidcAuth: identityOidcAuthService, + identityJwtAuth: identityJwtAuthService, accessApprovalPolicy: accessApprovalPolicyService, accessApprovalRequest: accessApprovalRequestService, secretApprovalPolicy: secretApprovalPolicyService, diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts new file mode 100644 index 000000000..6c9d2ae4a --- /dev/null +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -0,0 +1,86 @@ +import { z } from "zod"; + +import { IdentityJwtAuthsSchema } from "@app/db/schemas"; +import { JWT_AUTH } from "@app/lib/api-docs"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { JwtConfigurationType } from "@app/services/identity-jwt-auth/identity-jwt-auth-types"; +import { + validateJwtAuthAudiencesField, + validateJwtBoundClaimsField +} from "@app/services/identity-jwt-auth/identity-jwt-auth-validators"; + +const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({ + encryptedJwksCaCert: true, + encryptedPublicKeys: true +}).extend({ + jwksCaCert: z.string(), + publicKeys: z.string() +}); + +export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Attach JWT Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(JWT_AUTH.ATTACH.identityId) + }), + body: z.object({ + configurationType: z.nativeEnum(JwtConfigurationType).describe(JWT_AUTH.ATTACH.configurationType), + jwksUrl: z.string().describe(JWT_AUTH.ATTACH.jwksUrl), + jwksCaCert: z.string().describe(JWT_AUTH.ATTACH.jwksCaCert), + publicKeys: z.string().array().describe(JWT_AUTH.ATTACH.publicKeys), + boundIssuer: z.string().min(1).describe(JWT_AUTH.ATTACH.boundIssuer), + boundAudiences: validateJwtAuthAudiencesField.describe(JWT_AUTH.ATTACH.boundAudiences), + boundClaims: validateJwtBoundClaimsField.describe(JWT_AUTH.ATTACH.boundClaims), + boundSubject: z.string().optional().default("").describe(JWT_AUTH.ATTACH.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(JWT_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(1) + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenTTL must have a non zero number" + }) + .default(2592000) + .describe(JWT_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .default(2592000) + .describe(JWT_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit) + }), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema + }) + } + }, + handler: async (req) => {} + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index f9edfc18c..a04f77b7a 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -12,6 +12,7 @@ import { registerIdentityAccessTokenRouter } from "./identity-access-token-route import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; +import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityRouter } from "./identity-router"; @@ -54,6 +55,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter); + await authRouter.register(registerIdentityJwtAuthRouter); }, { prefix: "/auth" } ); diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts new file mode 100644 index 000000000..5e6d13be6 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityJwtAuthDALFactory = ReturnType; + +export const identityJwtAuthDALFactory = (db: TDbClient) => { + const jwtAuthOrm = ormify(db, TableName.IdentityJwtAuth); + + return jwtAuthOrm; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts new file mode 100644 index 000000000..c61ae6769 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -0,0 +1,137 @@ +import { ForbiddenError } from "@casl/ability"; + +import { IdentityAuthMethod } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; + +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal"; +import { TAttachJwtAuthDTO } from "./identity-jwt-auth-types"; + +type TIdentityJwtAuthServiceFactoryDep = { + identityJwtAuthDAL: TIdentityJwtAuthDALFactory; + identityOrgMembershipDAL: Pick; + identityAccessTokenDAL: Pick; + permissionService: Pick; + licenseService: Pick; + kmsService: Pick; +}; + +export type TIdentityJwtAuthServiceFactory = ReturnType; + +export const identityJwtAuthServiceFactory = ({ + identityJwtAuthDAL, + identityOrgMembershipDAL, + permissionService, + licenseService, + kmsService +}: TIdentityJwtAuthServiceFactoryDep) => { + const attachJwtAuth = async ({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TAttachJwtAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + } + if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { + throw new BadRequestError({ + message: "Failed to add JWT Auth to already configured identity" + }); + } + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({ + plainText: Buffer.from(jwksCaCert) + }); + + const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({ + plainText: Buffer.from(publicKeys.join(",")) + }); + + const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { + const doc = await identityJwtAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + configurationType, + jwksUrl, + encryptedJwksCaCert, + encryptedPublicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + + return doc; + }); + return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys }; + }; + + return { + attachJwtAuth + }; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts new file mode 100644 index 000000000..e06c56437 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts @@ -0,0 +1,22 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum JwtConfigurationType { + JWKS = "jwks", + STATIC = "static" +} + +export type TAttachJwtAuthDTO = { + identityId: string; + configurationType: JwtConfigurationType; + jwksUrl: string; + jwksCaCert: string; + publicKeys: string[]; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; +} & Omit; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts new file mode 100644 index 000000000..515c2ac7e --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const validateJwtAuthAudiencesField = z + .string() + .trim() + .default("") + .transform((data) => { + if (data === "") return ""; + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + +export const validateJwtBoundClaimsField = z.record(z.string()).transform((data) => { + const formattedClaims: Record = {}; + Object.keys(data).forEach((key) => { + formattedClaims[key] = data[key] + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + + return formattedClaims; +});