From 2c50de28bd1c01118bd8e547ef757563cb61af4b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 8 Jul 2025 18:28:43 +0400 Subject: [PATCH] feat(fips): fips validated JWT's --- .infisicalignore | 1 + backend/Dockerfile.dev.fips | 11 +- backend/e2e-test/vitest-environment-knex.ts | 4 +- .../assume-privilege-service.ts | 6 +- .../dynamic-secret/providers/github.ts | 3 +- .../ldap-config/ldap-config-service.ts | 4 +- .../ee/services/oidc/oidc-config-service.ts | 4 +- .../saml-config/saml-config-service.ts | 4 +- backend/src/ee/services/scim/scim-service.ts | 4 +- .../bitbucket-secret-scanning-service.ts | 5 +- .../crypto/cryptography/asymmetric-fips.ts | 124 +++++++ .../crypto.ts} | 325 +++--------------- .../src/lib/crypto/cryptography/hash-fips.ts | 107 ++++++ backend/src/lib/crypto/cryptography/index.ts | 9 + .../src/lib/crypto/cryptography/jwt-fips.ts | 289 ++++++++++++++++ backend/src/lib/crypto/cryptography/types.ts | 92 +++++ backend/src/lib/crypto/index.ts | 2 +- .../server/plugins/auth/inject-identity.ts | 5 +- backend/src/server/routes/v1/auth-router.ts | 4 +- backend/src/server/routes/v2/mfa-router.ts | 4 +- .../services/auth-token/auth-token-service.ts | 3 +- backend/src/services/auth/auth-fns.ts | 9 +- .../src/services/auth/auth-login-service.ts | 16 +- .../services/auth/auth-password-service.ts | 4 +- .../src/services/auth/auth-signup-service.ts | 12 +- .../identity-access-token-service.ts | 10 +- .../identity-alicloud-auth-service.ts | 4 +- .../identity-aws-auth-service.ts | 4 +- .../identity-azure-auth-fns.ts | 6 +- .../identity-azure-auth-service.ts | 4 +- .../identity-gcp-auth-fns.ts | 6 +- .../identity-gcp-auth-service.ts | 4 +- .../identity-jwt-auth-service.ts | 12 +- .../identity-kubernetes-auth-service.ts | 4 +- .../identity-ldap-auth-service.ts | 4 +- .../identity-oci-auth-service.ts | 4 +- .../identity-oidc-auth-service.ts | 10 +- .../identity-tls-cert-auth-service.ts | 3 +- .../identity-token-auth-service.ts | 4 +- .../identity-ua/identity-ua-service.ts | 3 +- .../integration-auth/integration-token.ts | 5 +- .../microsoft-teams/microsoft-teams-fns.ts | 8 +- backend/src/services/org/org-service.ts | 5 +- .../super-admin/super-admin-service.ts | 3 +- 44 files changed, 779 insertions(+), 375 deletions(-) create mode 100644 backend/src/lib/crypto/cryptography/asymmetric-fips.ts rename backend/src/lib/crypto/{cryptography.ts => cryptography/crypto.ts} (63%) create mode 100644 backend/src/lib/crypto/cryptography/hash-fips.ts create mode 100644 backend/src/lib/crypto/cryptography/index.ts create mode 100644 backend/src/lib/crypto/cryptography/jwt-fips.ts create mode 100644 backend/src/lib/crypto/cryptography/types.ts diff --git a/.infisicalignore b/.infisicalignore index 4c50aa7fa..06a8db5c2 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -47,3 +47,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:582 backend/src/services/smtp/smtp-service.ts:generic-api-key:79 frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx:cloudflare-api-key:7 docs/integrations/app-connections/zabbix.mdx:generic-api-key:91 +docs/integrations/app-connections/bitbucket.mdx:generic-api-key:123 diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index 27c35f2aa..23f9db295 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -1,5 +1,12 @@ FROM node:20-slim +# ? Setup a test SoftHSM module. In production a real HSM is used. + +ARG SOFTHSM2_VERSION=2.5.0 + +ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ + SOFTHSM2_SOURCES=/tmp/softhsm2 + # Install build dependencies including python3 (required for pkcs11js and partially TDS driver) RUN apt-get update && apt-get install -y \ build-essential \ @@ -45,9 +52,7 @@ RUN apt-get install -y opensc RUN mkdir -p /etc/softhsm2/tokens && \ softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 - - -# Build and install FIPS validated OpenSSL + WORKDIR /openssl-build RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ && tar -xf openssl-3.1.2.tar.gz \ diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 289945f17..60e70d379 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -2,7 +2,7 @@ import "ts-node/register"; import dotenv from "dotenv"; -import jwt from "jsonwebtoken"; +import { crypto } from "@app/lib/crypto/cryptography"; import path from "path"; import { seedData1 } from "@app/db/seed-data"; @@ -83,7 +83,7 @@ export default { // @ts-expect-error type globalThis.testSuperAdminDAL = superAdminDAL; // @ts-expect-error type - globalThis.jwtAuthToken = jwt.sign( + globalThis.jwtAuthToken = crypto.jwt().sign( { authTokenType: AuthTokenType.ACCESS_TOKEN, userId: seedData1.id, diff --git a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts index c1cfad082..a63b0e3be 100644 --- a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts +++ b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto/cryptography"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -62,7 +62,7 @@ export const assumePrivilegeServiceFactory = ({ }); const appCfg = getConfig(); - const assumePrivilegesToken = jwt.sign( + const assumePrivilegesToken = crypto.jwt().sign( { tokenVersionId, actorType: targetActorType, @@ -82,7 +82,7 @@ export const assumePrivilegeServiceFactory = ({ tokenVersionId ) => { const appCfg = getConfig(); - const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as { + const decodedToken = crypto.jwt().verify(token, appCfg.AUTH_SECRET) as { tokenVersionId: string; projectId: string; requesterId: string; diff --git a/backend/src/ee/services/dynamic-secret/providers/github.ts b/backend/src/ee/services/dynamic-secret/providers/github.ts index 172041f6c..8bd1cccf4 100644 --- a/backend/src/ee/services/dynamic-secret/providers/github.ts +++ b/backend/src/ee/services/dynamic-secret/providers/github.ts @@ -1,6 +1,7 @@ import axios from "axios"; import jwt from "jsonwebtoken"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; @@ -40,7 +41,7 @@ export const GithubProvider = (): TDynamicProviderFns => { let appJwt: string; try { - appJwt = jwt.sign(jwtPayload, privateKey, { algorithm: "RS256" }); + appJwt = crypto.jwt().sign(jwtPayload, privateKey, { algorithm: "RS256" }); } catch (error) { let message = "Failed to sign JWT."; if (error instanceof jwt.JsonWebTokenError) { diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 426ed132e..2a4f895c4 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,11 +1,11 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; @@ -536,7 +536,7 @@ export const ldapConfigServiceFactory = ({ const isUserCompleted = Boolean(user.isAccepted); const userEnc = await userDAL.findUserEncKeyByUserId(user.id); - const providerAuthToken = jwt.sign( + const providerAuthToken = crypto.jwt().sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 1a3374035..ca6f67235 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; @@ -13,6 +12,7 @@ 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-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; @@ -406,7 +406,7 @@ export const oidcConfigServiceFactory = ({ const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const isUserCompleted = Boolean(user.isAccepted); - const providerAuthToken = jwt.sign( + const providerAuthToken = crypto.jwt().sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 5430b0afc..a89bc0346 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -1,8 +1,8 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; @@ -419,7 +419,7 @@ export const samlConfigServiceFactory = ({ const isUserCompleted = Boolean(user.isAccepted); const userEnc = await userDAL.findUserEncKeyByUserId(user.id); - const providerAuthToken = jwt.sign( + const providerAuthToken = crypto.jwt().sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 6c5465488..fc05cd2ce 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -1,6 +1,5 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import jwt from "jsonwebtoken"; import { scimPatch } from "scim-patch"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas"; @@ -10,6 +9,7 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; +import { crypto } from "@app/lib/crypto"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; @@ -137,7 +137,7 @@ export const scimServiceFactory = ({ ttlDays }); - const scimToken = jwt.sign( + const scimToken = crypto.jwt().sign( { scimTokenId: scimTokenData.id, authTokenType: AuthTokenType.SCIM_TOKEN diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts index c5a0aedd6..24b017d98 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-service.ts @@ -1,8 +1,7 @@ -import crypto from "crypto"; - import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal"; import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums"; import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue"; +import { crypto } from "@app/lib/crypto"; import { logger } from "@app/lib/logger"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; @@ -67,7 +66,7 @@ export const bitbucketSecretScanningService = ( const credentials = JSON.parse(decryptedCredentials.toString()) as TBitbucketDataSourceCredentials; - const hmac = crypto.createHmac("sha256", credentials.webhookSecret); + const hmac = crypto.rawCrypto.createHmac("sha256", credentials.webhookSecret); hmac.update(bodyString); const calculatedSignature = hmac.digest("hex"); diff --git a/backend/src/lib/crypto/cryptography/asymmetric-fips.ts b/backend/src/lib/crypto/cryptography/asymmetric-fips.ts new file mode 100644 index 000000000..9e1e12459 --- /dev/null +++ b/backend/src/lib/crypto/cryptography/asymmetric-fips.ts @@ -0,0 +1,124 @@ +import crypto from "crypto"; + +import { SecretEncryptionAlgo } from "@app/db/schemas"; +import { CryptographyError } from "@app/lib/errors"; + +export const asymmetricFipsValidated = () => { + const generateKeyPair = () => { + const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519"); + + return { + publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"), + privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64") + }; + }; + + const encryptAsymmetric = (data: string, publicKey: string, privateKey: string) => { + const pubKeyObj = crypto.createPublicKey({ + key: Buffer.from(publicKey, "base64"), + type: "spki", + format: "der" + }); + + const privKeyObj = crypto.createPrivateKey({ + key: Buffer.from(privateKey, "base64"), + type: "pkcs8", + format: "der" + }); + + // Generate shared secret using x25519 curve + const sharedSecret = crypto.diffieHellman({ + privateKey: privKeyObj, + publicKey: pubKeyObj + }); + + const nonce = crypto.randomBytes(24); + + // Derive 32-byte key from shared secret + const key = crypto.createHash("sha256").update(sharedSecret).digest(); + + // Use first 12 bytes of nonce as IV for AES-GCM + const iv = nonce.subarray(0, 12); + + // Encrypt with AES-256-GCM + const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv); + + const ciphertext = cipher.update(data, "utf8"); + cipher.final(); + + const authTag = cipher.getAuthTag(); + + // Combine ciphertext and auth tag + const combined = Buffer.concat([ciphertext, authTag]); + + return { + ciphertext: combined.toString("base64"), + nonce: nonce.toString("base64") + }; + }; + + const decryptAsymmetric = ({ + ciphertext, + nonce, + publicKey, + privateKey + }: { + ciphertext: string; + nonce: string; + publicKey: string; + privateKey: string; + }) => { + // Convert base64 keys back to key objects + const pubKeyObj = crypto.createPublicKey({ + key: Buffer.from(publicKey, "base64"), + type: "spki", + format: "der" + }); + + const privKeyObj = crypto.createPrivateKey({ + key: Buffer.from(privateKey, "base64"), + type: "pkcs8", + format: "der" + }); + + // Generate same shared secret + const sharedSecret = crypto.diffieHellman({ + privateKey: privKeyObj, + publicKey: pubKeyObj + }); + + const nonceBuffer = Buffer.from(nonce, "base64"); + const combinedBuffer = Buffer.from(ciphertext, "base64"); + + // Split ciphertext and auth tag (last 16 bytes for GCM) + const actualCiphertext = combinedBuffer.subarray(0, -16); + const authTag = combinedBuffer.subarray(-16); + + // Derive same 32-byte key + const key = crypto.createHash("sha256").update(sharedSecret).digest(); + + // Use first 12 bytes of nonce as IV + const iv = nonceBuffer.subarray(0, 12); + + // Decrypt + const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); + decipher.setAuthTag(authTag); + + const plaintext = decipher.update(actualCiphertext); + + try { + const final = decipher.final(); + return Buffer.concat([plaintext, final]).toString("utf8"); + } catch (error) { + throw new CryptographyError({ + message: "Invalid ciphertext or keys" + }); + } + }; + + return { + generateKeyPair, + encryptAsymmetric, + decryptAsymmetric + }; +}; diff --git a/backend/src/lib/crypto/cryptography.ts b/backend/src/lib/crypto/cryptography/crypto.ts similarity index 63% rename from backend/src/lib/crypto/cryptography.ts rename to backend/src/lib/crypto/cryptography/crypto.ts index d35991c49..f5c2182e6 100644 --- a/backend/src/lib/crypto/cryptography.ts +++ b/backend/src/lib/crypto/cryptography/crypto.ts @@ -4,6 +4,7 @@ import crypto, { subtle } from "node:crypto"; import bcrypt from "bcrypt"; import cryptoJs from "crypto-js"; +import jwtDep from "jsonwebtoken"; import nacl from "tweetnacl"; import naclUtils from "tweetnacl-util"; @@ -12,277 +13,30 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service"; -import { isBase64 } from "../base64"; -import { getConfig } from "../config/env"; -import { CryptographyError } from "../errors"; -import { logger } from "../logger"; - -enum DigestType { - Hex = "hex", - Base64 = "base64" -} - -export enum SymmetricKeySize { - Bits128 = "128-bits", - Bits256 = "256-bits" -} - -type TDecryptSymmetricInput = - | { - ciphertext: string; - iv: string; - tag: string; - key: string | Buffer; // can be hex encoded or buffer - keySize: SymmetricKeySize.Bits128; - } - | { - ciphertext: string; - iv: string; - tag: string; - key: string; // must be base64 encoded - keySize: SymmetricKeySize.Bits256; - }; - -type TEncryptSymmetricInput = - | { - plaintext: string; - key: string; - keySize: SymmetricKeySize.Bits256; - } - | { - plaintext: string; - key: string | Buffer; - keySize: SymmetricKeySize.Bits128; - }; - -type TDecryptAsymmetricInput = { - ciphertext: string; - nonce: string; - publicKey: string; - privateKey: string; -}; +import { isBase64 } from "../../base64"; +import { getConfig } from "../../config/env"; +import { CryptographyError } from "../../errors"; +import { logger } from "../../logger"; +import { asymmetricFipsValidated } from "./asymmetric-fips"; +import { hasherFipsValidated } from "./hash-fips"; +import { jwtFipsValidated } from "./jwt-fips"; +import { + DigestType, + JWTPayload, + JWTSecretOrKey, + JWTSignOptions, + JWTVerifyOptions, + SymmetricKeySize, + TDecryptAsymmetricInput, + TDecryptSymmetricInput, + TEncryptSymmetricInput +} from "./types"; const bytesToBits = (bytes: number) => bytes * 8; const IV_BYTES_SIZE = 12; const BLOCK_SIZE_BYTES_16 = 16; -const hasherFipsValidated = () => { - const keySize = 32; - - // For the salt when using pkdf2, we do salt rounds^6. If the salt rounds are 10, this will result in 10^6 = 1.000.000 iterations. - // The reason for this is because pbkdf2 is not as compute intense as bcrypt, making it faster to brute-force. - // From my testing, doing salt rounds^6 brings the computational power required to a little more than bcrypt. - // OWASP recommends a minimum of 600.000 iterations for pbkdf2, so 1.000.000 is more than enough. - // Ref: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 - const MIN_COST_FACTOR = 10; - const MAX_COST_FACTOR = 20; // Iterations scales polynomial (costFactor^6), so we need an upper bound - - const $calculateIterations = (costFactor: number) => { - return Math.round(costFactor ** 6); - }; - - const $hashPassword = (password: Buffer, salt: Buffer, iterations: number, keyLength: number) => { - return new Promise((resolve, reject) => { - crypto.pbkdf2(password, salt, iterations, keyLength, "sha256", (err, derivedKey) => { - if (err) { - return reject(err); - } - resolve(derivedKey); - }); - }); - }; - - const $validatePassword = async ( - inputPassword: Buffer, - storedHash: Buffer, - salt: Buffer, - iterations: number, - keyLength: number - ) => { - const computedHash = await $hashPassword(inputPassword, salt, iterations, keyLength); - - return crypto.timingSafeEqual(computedHash, storedHash); - }; - - const hash = async (password: string, costFactor: number) => { - // Strict input validation - if (typeof password !== "string" || password.length === 0) { - throw new CryptographyError({ - message: "Invalid input, password must be a non-empty string" - }); - } - - if (!Number.isInteger(costFactor)) { - throw new CryptographyError({ - message: "Invalid cost factor, must be an integer" - }); - } - - if (costFactor < MIN_COST_FACTOR || costFactor > MAX_COST_FACTOR) { - throw new CryptographyError({ - message: `Invalid cost factor, must be between ${MIN_COST_FACTOR} and ${MAX_COST_FACTOR}` - }); - } - - const iterations = $calculateIterations(costFactor); - - const salt = crypto.randomBytes(16); - const derivedKey = await $hashPassword(Buffer.from(password), salt, iterations, keySize); - - const combined = Buffer.concat([salt, derivedKey]); - return `$v1$${costFactor}$${combined.toString("base64")}`; // Store original costFactor! - }; - - const compare = async (password: string, hashedPassword: string) => { - try { - if (!hashedPassword?.startsWith("$v1$")) return false; - - const parts = hashedPassword.split("$"); - if (parts.length !== 4) return false; - - const [, , storedCostFactor, combined] = parts; - - if ( - !Number.isInteger(Number(storedCostFactor)) || - Number(storedCostFactor) < MIN_COST_FACTOR || - Number(storedCostFactor) > MAX_COST_FACTOR - ) { - return false; - } - - const combinedBuffer = Buffer.from(combined, "base64"); - const salt = combinedBuffer.subarray(0, 16); - const storedHash = combinedBuffer.subarray(16); - - const iterations = $calculateIterations(Number(storedCostFactor)); - - const isMatch = await $validatePassword(Buffer.from(password), storedHash, salt, iterations, keySize); - - return isMatch; - } catch { - return false; - } - }; - - return { - hash, - compare - }; -}; - -const generateAsymmetricKeyPairFipsValidated = () => { - const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519"); - - return { - publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"), - privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64") - }; -}; - -const encryptAsymmetricFipsValidated = (data: string, publicKey: string, privateKey: string) => { - const pubKeyObj = crypto.createPublicKey({ - key: Buffer.from(publicKey, "base64"), - type: "spki", - format: "der" - }); - - const privKeyObj = crypto.createPrivateKey({ - key: Buffer.from(privateKey, "base64"), - type: "pkcs8", - format: "der" - }); - - // Generate shared secret using x25519 curve - const sharedSecret = crypto.diffieHellman({ - privateKey: privKeyObj, - publicKey: pubKeyObj - }); - - const nonce = crypto.randomBytes(24); - - // Derive 32-byte key from shared secret - const key = crypto.createHash("sha256").update(sharedSecret).digest(); - - // Use first 12 bytes of nonce as IV for AES-GCM - const iv = nonce.subarray(0, 12); - - // Encrypt with AES-256-GCM - const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv); - - const ciphertext = cipher.update(data, "utf8"); - cipher.final(); - - const authTag = cipher.getAuthTag(); - - // Combine ciphertext and auth tag - const combined = Buffer.concat([ciphertext, authTag]); - - return { - ciphertext: combined.toString("base64"), - nonce: nonce.toString("base64") - }; -}; - -const decryptAsymmetricFipsValidated = ({ - ciphertext, - nonce, - publicKey, - privateKey -}: { - ciphertext: string; - nonce: string; - publicKey: string; - privateKey: string; -}) => { - // Convert base64 keys back to key objects - const pubKeyObj = crypto.createPublicKey({ - key: Buffer.from(publicKey, "base64"), - type: "spki", - format: "der" - }); - - const privKeyObj = crypto.createPrivateKey({ - key: Buffer.from(privateKey, "base64"), - type: "pkcs8", - format: "der" - }); - - // Generate same shared secret - const sharedSecret = crypto.diffieHellman({ - privateKey: privKeyObj, - publicKey: pubKeyObj - }); - - const nonceBuffer = Buffer.from(nonce, "base64"); - const combinedBuffer = Buffer.from(ciphertext, "base64"); - - // Split ciphertext and auth tag (last 16 bytes for GCM) - const actualCiphertext = combinedBuffer.subarray(0, -16); - const authTag = combinedBuffer.subarray(-16); - - // Derive same 32-byte key - const key = crypto.createHash("sha256").update(sharedSecret).digest(); - - // Use first 12 bytes of nonce as IV - const iv = nonceBuffer.subarray(0, 12); - - // Decrypt - const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); - decipher.setAuthTag(authTag); - - const plaintext = decipher.update(actualCiphertext); - - try { - const final = decipher.final(); - return Buffer.concat([plaintext, final]).toString("utf8"); - } catch (error) { - throw new CryptographyError({ - message: "Invalid ciphertext or keys" - }); - } -}; - const generateAsymmetricKeyPairNoFipsValidation = () => { const pair = nacl.box.keyPair(); @@ -447,21 +201,21 @@ const cryptographyFactory = () => { const asymmetric = () => { const generateKeyPair = () => { if (isFipsModeEnabled()) { - return generateAsymmetricKeyPairFipsValidated(); + return asymmetricFipsValidated().generateKeyPair(); } return generateAsymmetricKeyPairNoFipsValidation(); }; const encrypt = (data: string, publicKey: string, privateKey: string) => { if (isFipsModeEnabled()) { - return encryptAsymmetricFipsValidated(data, publicKey, privateKey); + return asymmetricFipsValidated().encryptAsymmetric(data, publicKey, privateKey); } return encryptAsymmetricNoFipsValidation(data, publicKey, privateKey); }; const decrypt = ({ ciphertext, nonce, publicKey, privateKey }: TDecryptAsymmetricInput) => { if (isFipsModeEnabled()) { - return decryptAsymmetricFipsValidated({ ciphertext, nonce, publicKey, privateKey }); + return asymmetricFipsValidated().decryptAsymmetric({ ciphertext, nonce, publicKey, privateKey }); } return decryptAsymmetricNoFipsValidation({ ciphertext, nonce, publicKey, privateKey }); }; @@ -633,13 +387,44 @@ const cryptographyFactory = () => { compareHash }; }; + const jwt = () => { + $checkIsInitialized(); + + const sign = (payload: JWTPayload, secretOrKey: JWTSecretOrKey, options: JWTSignOptions = {}) => { + if (isFipsModeEnabled()) { + return jwtFipsValidated().sign(payload, secretOrKey, options); + } + return jwtDep.sign(payload, secretOrKey, options); + }; + + const verify = (token: string, secretOrKey: JWTSecretOrKey, options: JWTVerifyOptions = {}) => { + if (isFipsModeEnabled()) { + return jwtFipsValidated().verify(token, secretOrKey, options); + } + return jwtDep.verify(token, secretOrKey, options); + }; + + const decode = (token: string, options: { complete?: boolean } = {}) => { + if (isFipsModeEnabled()) { + return jwtFipsValidated().decode(token, options); + } + return jwtDep.decode(token, options); + }; + + return { + sign, + verify, + decode + }; + }; return { initialize, isFipsModeEnabled, - hashing, verifyFipsLicense, + hashing, encryption, + jwt, randomBytes: crypto.randomBytes, randomInt: crypto.randomInt, rawCrypto: { @@ -674,6 +459,4 @@ const cryptographyFactory = () => { const factoryInstance = cryptographyFactory(); -export type TCryptographyFactory = ReturnType; - export { factoryInstance as crypto, DigestType }; diff --git a/backend/src/lib/crypto/cryptography/hash-fips.ts b/backend/src/lib/crypto/cryptography/hash-fips.ts new file mode 100644 index 000000000..d4ec5f727 --- /dev/null +++ b/backend/src/lib/crypto/cryptography/hash-fips.ts @@ -0,0 +1,107 @@ +import crypto from "crypto"; + +import { CryptographyError } from "@app/lib/errors"; + +export const hasherFipsValidated = () => { + const keySize = 32; + + // For the salt when using pkdf2, we do salt rounds^6. If the salt rounds are 10, this will result in 10^6 = 1.000.000 iterations. + // The reason for this is because pbkdf2 is not as compute intense as bcrypt, making it faster to brute-force. + // From my testing, doing salt rounds^6 brings the computational power required to a little more than bcrypt. + // OWASP recommends a minimum of 600.000 iterations for pbkdf2, so 1.000.000 is more than enough. + // Ref: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 + const MIN_COST_FACTOR = 10; + const MAX_COST_FACTOR = 20; // Iterations scales polynomial (costFactor^6), so we need an upper bound + + const $calculateIterations = (costFactor: number) => { + return Math.round(costFactor ** 6); + }; + + const $hashPassword = (password: Buffer, salt: Buffer, iterations: number, keyLength: number) => { + return new Promise((resolve, reject) => { + crypto.pbkdf2(password, salt, iterations, keyLength, "sha256", (err, derivedKey) => { + if (err) { + return reject(err); + } + resolve(derivedKey); + }); + }); + }; + + const $validatePassword = async ( + inputPassword: Buffer, + storedHash: Buffer, + salt: Buffer, + iterations: number, + keyLength: number + ) => { + const computedHash = await $hashPassword(inputPassword, salt, iterations, keyLength); + + return crypto.timingSafeEqual(computedHash, storedHash); + }; + + const hash = async (password: string, costFactor: number) => { + // Strict input validation + if (typeof password !== "string" || password.length === 0) { + throw new CryptographyError({ + message: "Invalid input, password must be a non-empty string" + }); + } + + if (!Number.isInteger(costFactor)) { + throw new CryptographyError({ + message: "Invalid cost factor, must be an integer" + }); + } + + if (costFactor < MIN_COST_FACTOR || costFactor > MAX_COST_FACTOR) { + throw new CryptographyError({ + message: `Invalid cost factor, must be between ${MIN_COST_FACTOR} and ${MAX_COST_FACTOR}` + }); + } + + const iterations = $calculateIterations(costFactor); + + const salt = crypto.randomBytes(16); + const derivedKey = await $hashPassword(Buffer.from(password), salt, iterations, keySize); + + const combined = Buffer.concat([salt, derivedKey]); + return `$v1$${costFactor}$${combined.toString("base64")}`; // Store original costFactor! + }; + + const compare = async (password: string, hashedPassword: string) => { + try { + if (!hashedPassword?.startsWith("$v1$")) return false; + + const parts = hashedPassword.split("$"); + if (parts.length !== 4) return false; + + const [, , storedCostFactor, combined] = parts; + + if ( + !Number.isInteger(Number(storedCostFactor)) || + Number(storedCostFactor) < MIN_COST_FACTOR || + Number(storedCostFactor) > MAX_COST_FACTOR + ) { + return false; + } + + const combinedBuffer = Buffer.from(combined, "base64"); + const salt = combinedBuffer.subarray(0, 16); + const storedHash = combinedBuffer.subarray(16); + + const iterations = $calculateIterations(Number(storedCostFactor)); + + const isMatch = await $validatePassword(Buffer.from(password), storedHash, salt, iterations, keySize); + + return isMatch; + } catch { + return false; + } + }; + + return { + hash, + compare + }; +}; diff --git a/backend/src/lib/crypto/cryptography/index.ts b/backend/src/lib/crypto/cryptography/index.ts new file mode 100644 index 000000000..5edd5fb89 --- /dev/null +++ b/backend/src/lib/crypto/cryptography/index.ts @@ -0,0 +1,9 @@ +export { crypto } from "./crypto"; +export { + DigestType, + SymmetricKeySize, + TDecryptAsymmetricInput, + TDecryptSymmetricInput, + TEncryptedWithRootEncryptionKey, + TEncryptSymmetricInput +} from "./types"; diff --git a/backend/src/lib/crypto/cryptography/jwt-fips.ts b/backend/src/lib/crypto/cryptography/jwt-fips.ts new file mode 100644 index 000000000..42e5809c3 --- /dev/null +++ b/backend/src/lib/crypto/cryptography/jwt-fips.ts @@ -0,0 +1,289 @@ +import crypto from "crypto"; +import RE2 from "re2"; + +import { Algorithm, CompleteJWTPayload, JWTPayload, JWTSecretOrKey, JWTSignOptions, JWTVerifyOptions } from "./types"; + +export const jwtFipsValidated = () => { + const $base64urlEncode = (str: string) => Buffer.from(str).toString("base64url"); + const $isRSAAlgorithm = (algorithm: string) => algorithm.startsWith("RS"); + + const $parseTimeToSeconds = (timeStr: string | number) => { + if (typeof timeStr === "number") { + return timeStr; + } + + const match = new RE2(/^(\d+)([smhd])$/).exec(timeStr); + if (!match) { + throw new Error(`Invalid time format: ${timeStr}`); + } + + const value = parseInt(match[1], 10); + const unit = match[2]; + + switch (unit) { + case "s": + return value; + case "m": + return value * 60; + case "h": + return value * 60 * 60; + case "d": + return value * 60 * 60 * 24; + default: + throw new Error(`Unknown time unit: ${unit}`); + } + }; + + const $getHashAlgorithm = (algorithm: string) => { + switch (algorithm) { + case "HS256": + case "RS256": + return "sha256"; + case "HS384": + case "RS384": + return "sha384"; + case "HS512": + case "RS512": + return "sha512"; + default: + throw new Error(`Unsupported algorithm: ${algorithm}`); + } + }; + + /** + * Sign a JWT token + */ + const sign = (payload: JWTPayload, secretOrPrivateKey: JWTSecretOrKey, options: JWTSignOptions = {}) => { + const algorithm = options.algorithm || "HS256"; + + const now = Math.floor(Date.now() / 1000); + // Create header + const header = { + alg: algorithm, + typ: "JWT", + ...(options.keyid && { kid: options.keyid }) + }; + + // Create payload with timestamps + const finalPayload = { + ...payload, + iat: now, + ...(options.expiresIn !== undefined && { exp: now + $parseTimeToSeconds(options.expiresIn) }) + }; + + // Encode header and payload + const encodedHeader = $base64urlEncode(JSON.stringify(header)); + const encodedPayload = $base64urlEncode(JSON.stringify(finalPayload)); + + // Create signature + const data = `${encodedHeader}.${encodedPayload}`; + const hashAlgorithm = $getHashAlgorithm(algorithm); + let signature: string; + + if ($isRSAAlgorithm(algorithm)) { + let privateKey: crypto.KeyLike | { key: string | Buffer }; + + if (typeof secretOrPrivateKey === "string") { + try { + // Try to create a proper private key object + privateKey = crypto.createPrivateKey(secretOrPrivateKey); + } catch (error) { + throw new Error("Invalid JWT private key"); + } + } else { + privateKey = secretOrPrivateKey; + } + + const signatureBuffer = crypto.sign(hashAlgorithm, Buffer.from(data), privateKey); + signature = signatureBuffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + } else { + // HMAC signing + if (typeof secretOrPrivateKey !== "string") { + throw new Error("HMAC algorithms require a string secret"); + } + signature = crypto + .createHmac(hashAlgorithm, secretOrPrivateKey) + .update(data) + .digest("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + } + + return `${data}.${signature}`; + }; + + const verify = (token: string, secretOrKey: JWTSecretOrKey, options: JWTVerifyOptions = {}) => { + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + + const [encodedHeader, encodedPayload, signature] = parts; + + // Decode header + const headerJson = Buffer.from(encodedHeader, "base64").toString(); + const header = JSON.parse(headerJson) as { alg: string; typ: string; kid?: string }; + + if (!header.alg || header.typ !== "JWT") { + throw new Error("Invalid JWT header"); + } + + // Extract the actual key from different input types + let keyString: string; + if (Buffer.isBuffer(secretOrKey)) { + keyString = secretOrKey.toString(); + } else if (typeof secretOrKey === "object" && "key" in secretOrKey) { + keyString = Buffer.isBuffer(secretOrKey.key) ? secretOrKey.key.toString() : secretOrKey.key; + } else { + keyString = secretOrKey as string; + } + + // Verify signature + const data = `${encodedHeader}.${encodedPayload}`; + const hashAlgorithm = $getHashAlgorithm(header.alg); + let isValidSignature: boolean; + + if ($isRSAAlgorithm(header.alg)) { + // For RSA, handle both private and public keys + let verificationKey: crypto.KeyLike; + + // Clean up the key format + const cleanKey = keyString.replace(/\\n/g, "\n"); + + try { + // If it's a private key, extract the public key for verification + if (cleanKey.includes("PRIVATE KEY")) { + const privateKeyObj = crypto.createPrivateKey(cleanKey); + verificationKey = crypto.createPublicKey(privateKeyObj); + } else { + // It's already a public key + verificationKey = crypto.createPublicKey(cleanKey); + } + } catch (error) { + throw new Error("Invalid JWT signature"); + } + + // Convert base64url signature back to buffer + const signatureBuffer = Buffer.from( + signature.replace(/-/g, "+").replace(/_/g, "/") + "==".slice(0, (4 - (signature.length % 4)) % 4), + "base64" + ); + isValidSignature = crypto.verify(hashAlgorithm, Buffer.from(data), verificationKey, signatureBuffer); + } else { + // HMAC verification + const expectedSignature = crypto + .createHmac(hashAlgorithm, keyString) + .update(data) + .digest("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + isValidSignature = signature === expectedSignature; + } + + if (!isValidSignature) { + throw new Error("Invalid JWT signature"); + } + + // Decode payload + const payloadJson = Buffer.from(encodedPayload, "base64").toString(); + const payload = JSON.parse(payloadJson) as JWTPayload & { + aud?: string | string[]; + iss?: string; + sub?: string; + nbf?: number; + jti?: string; + }; + + const now = Math.floor(Date.now() / 1000); + const clockTolerance = options.clockTolerance || 0; + + // Check expiration + if (!options.ignoreExpiration && payload.exp && now - clockTolerance > payload.exp) { + throw new Error("JWT token has expired"); + } + + // Check not before + if (!options.ignoreNotBefore && payload.nbf && now + clockTolerance < payload.nbf) { + throw new Error("JWT not active"); + } + + // Check audience + if (options.audience) { + const audiences = Array.isArray(options.audience) ? options.audience : [options.audience]; + const tokenAudiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; + const hasValidAudience = audiences.some((aud) => tokenAudiences.includes(aud)); + if (!hasValidAudience) { + throw new Error("JWT audience invalid"); + } + } + + // Check issuer + if (options.issuer) { + const issuers = Array.isArray(options.issuer) ? options.issuer : [options.issuer]; + if (!payload.iss || !issuers.includes(payload.iss)) { + throw new Error("JWT issuer invalid"); + } + } + + // Check subject + if (options.subject && payload.sub !== options.subject) { + throw new Error("JWT subject invalid"); + } + + // Check JWT ID + if (options.jwtid && payload.jti !== options.jwtid) { + throw new Error("JWT ID invalid"); + } + + // Check max age + if (options.maxAge && payload.iat) { + const maxAgeSeconds = typeof options.maxAge === "string" ? $parseTimeToSeconds(options.maxAge) : options.maxAge; + if (now - payload.iat > maxAgeSeconds) { + throw new Error("JWT max age exceeded"); + } + } + + // Check algorithms + if (options.algorithms && !options.algorithms.includes(header.alg as Algorithm)) { + throw new Error(`Algorithm not allowed: ${header.alg}`); + } + + return payload; + }; + + const decode = (token: string, options: { complete?: boolean } = {}): JWTPayload | CompleteJWTPayload => { + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + + const [encodedHeader, encodedPayload] = parts; + + // Decode header + const headerJson = Buffer.from(encodedHeader, "base64").toString(); + const header = JSON.parse(headerJson) as Record; + + // Decode payload + const payloadJson = Buffer.from(encodedPayload, "base64").toString(); + const payload = JSON.parse(payloadJson) as Record; + + // Return complete token info or just payload + if (options.complete) { + return { + header, + payload, + signature: parts[2] + }; + } + + return payload as JWTPayload; + }; + + return { + sign, + verify, + decode + }; +}; diff --git a/backend/src/lib/crypto/cryptography/types.ts b/backend/src/lib/crypto/cryptography/types.ts new file mode 100644 index 000000000..e9622575e --- /dev/null +++ b/backend/src/lib/crypto/cryptography/types.ts @@ -0,0 +1,92 @@ +import { KeyObject } from "crypto"; + +import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; + +export enum DigestType { + Hex = "hex", + Base64 = "base64" +} + +export enum SymmetricKeySize { + Bits128 = "128-bits", + Bits256 = "256-bits" +} + +export type TDecryptSymmetricInput = + | { + ciphertext: string; + iv: string; + tag: string; + key: string | Buffer; // can be hex encoded or buffer + keySize: SymmetricKeySize.Bits128; + } + | { + ciphertext: string; + iv: string; + tag: string; + key: string; // must be base64 encoded + keySize: SymmetricKeySize.Bits256; + }; + +export type TEncryptSymmetricInput = + | { + plaintext: string; + key: string; + keySize: SymmetricKeySize.Bits256; + } + | { + plaintext: string; + key: string | Buffer; + keySize: SymmetricKeySize.Bits128; + }; + +export type TDecryptAsymmetricInput = { + ciphertext: string; + nonce: string; + publicKey: string; + privateKey: string; +}; + +export type TEncryptedWithRootEncryptionKey = { + iv: string; + tag: string; + ciphertext: string; + algorithm: SecretEncryptionAlgo; + encoding: SecretKeyEncoding; +}; + +export interface JWTPayload { + iat?: number; + exp?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +export interface CompleteJWTPayload { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + header: any; + payload: JWTPayload; + signature: string; +} + +export type Algorithm = "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512"; + +export interface JWTSignOptions { + algorithm?: Algorithm | undefined; + keyid?: string | undefined; + expiresIn?: string | number; +} + +export type JWTSecretOrKey = string | Buffer | KeyObject | { key: string | Buffer; passphrase: string }; + +export interface JWTVerifyOptions { + algorithms?: Algorithm[] | undefined; + audience?: string | string[]; + issuer?: string | string[]; + subject?: string; + ignoreExpiration?: boolean; + ignoreNotBefore?: boolean; + clockTolerance?: number; + maxAge?: string | number; + jwtid?: string; +} diff --git a/backend/src/lib/crypto/index.ts b/backend/src/lib/crypto/index.ts index aac797a3e..d67e75d0b 100644 --- a/backend/src/lib/crypto/index.ts +++ b/backend/src/lib/crypto/index.ts @@ -1,4 +1,4 @@ -export { crypto, SymmetricKeySize } from "./cryptography"; +export { crypto, SymmetricKeySize, TEncryptedWithRootEncryptionKey } from "./cryptography"; export { buildSecretBlindIndexFromName } from "./encryption"; export { decryptIntegrationAuths, diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index f065bfbed..98982e969 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -1,11 +1,12 @@ import { requestContext } from "@fastify/request-context"; import { FastifyRequest } from "fastify"; import fp from "fastify-plugin"; -import jwt, { JwtPayload } from "jsonwebtoken"; import { TServiceTokens, TUsers } from "@app/db/schemas"; import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; +import { JWTPayload } from "@app/lib/crypto/cryptography/types"; import { BadRequestError } from "@app/lib/errors"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; @@ -72,7 +73,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { } as const; } - const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload; + const decodedToken = crypto.jwt().verify(authTokenValue, jwtSecret) as JWTPayload; switch (decodedToken.authTokenType) { case AuthTokenType.ACCESS_TOKEN: diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 7231ce85c..e7c06ff51 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -1,7 +1,7 @@ -import jwt from "jsonwebtoken"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { getMinExpiresIn } from "@app/lib/fn"; import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -93,7 +93,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { } } - const token = jwt.sign( + const token = crypto.jwt().sign( { authMethod: decodedToken.authMethod, authTokenType: AuthTokenType.ACCESS_TOKEN, diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts index 6f28ec34c..bdb609432 100644 --- a/backend/src/server/routes/v2/mfa-router.ts +++ b/backend/src/server/routes/v2/mfa-router.ts @@ -1,7 +1,7 @@ -import jwt from "jsonwebtoken"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { mfaRateLimit } from "@app/server/config/rateLimiter"; import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type"; @@ -23,7 +23,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { return res; } - const decodedToken = jwt.verify(token, cfg.AUTH_SECRET) as AuthModeMfaJwtTokenPayload; + const decodedToken = crypto.jwt().verify(token, cfg.AUTH_SECRET) as AuthModeMfaJwtTokenPayload; if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN) throw new Error("Unauthorized access"); const user = await server.store.user.findById(decodedToken.userId); diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 507095248..1a2f290ec 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,4 +1,3 @@ -import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; @@ -160,7 +159,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu message: "Failed to find refresh token" }); - const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; + const decodedToken = crypto.jwt().verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw new UnauthorizedError({ diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index ec6e0a303..b38275c8b 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -1,6 +1,5 @@ -import jwt from "jsonwebtoken"; - import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; @@ -8,7 +7,7 @@ import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, Au export const validateProviderAuthToken = (providerToken: string, username?: string) => { if (!providerToken) throw new UnauthorizedError(); const appCfg = getConfig(); - const decodedToken = jwt.verify(providerToken, appCfg.AUTH_SECRET) as AuthModeProviderJwtTokenPayload; + const decodedToken = crypto.jwt().verify(providerToken, appCfg.AUTH_SECRET) as AuthModeProviderJwtTokenPayload; if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new UnauthorizedError(); @@ -38,7 +37,7 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid }); } - const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload; + const decodedToken = crypto.jwt().verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload; if (!validate) return decodedToken; if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError(); @@ -64,7 +63,7 @@ export const validatePasswordResetAuthorization = (token?: string) => { }); } - const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload; + const decodedToken = crypto.jwt().verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload; if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) { throw new UnauthorizedError({ diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index a70fa5cc2..8463562bc 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,4 +1,3 @@ -import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas"; @@ -6,8 +5,7 @@ import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/a import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; -import { crypto } from "@app/lib/crypto/cryptography"; +import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn"; @@ -156,7 +154,7 @@ export const authLoginServiceFactory = ({ } } - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { authMethod, authTokenType: AuthTokenType.ACCESS_TOKEN, @@ -171,7 +169,7 @@ export const authLoginServiceFactory = ({ { expiresIn: tokenSessionExpiresIn } ); - const refreshToken = jwt.sign( + const refreshToken = crypto.jwt().sign( { authMethod, authTokenType: AuthTokenType.REFRESH_TOKEN, @@ -390,7 +388,7 @@ export const authLoginServiceFactory = ({ authJwtToken = authJwtToken.replace("Bearer ", ""); // remove bearer from token // The decoded JWT token, which contains the auth method. - const decodedToken = jwt.verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload; + const decodedToken = crypto.jwt().verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload; if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" }); const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId); @@ -415,7 +413,7 @@ export const authLoginServiceFactory = ({ if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) { enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd); - const mfaToken = jwt.sign( + const mfaToken = crypto.jwt().sign( { authMethod: decodedToken.authMethod, authTokenType: AuthTokenType.MFA_TOKEN, @@ -626,7 +624,7 @@ export const authLoginServiceFactory = ({ throw err; } - const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload; + const decodedToken = crypto.jwt().verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload; const userEnc = await userDAL.findUserEncKeyByUserId(userId); if (!userEnc) throw new Error("Failed to authenticate user"); @@ -776,7 +774,7 @@ export const authLoginServiceFactory = ({ const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const isUserCompleted = user.isAccepted; - const providerAuthToken = jwt.sign( + const providerAuthToken = crypto.jwt().sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index bd8a9a23d..39ab0a134 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -1,5 +1,3 @@ -import jwt from "jsonwebtoken"; - import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; @@ -174,7 +172,7 @@ export const authPaswordServiceFactory = ({ code }); - const token = jwt.sign( + const token = crypto.jwt().sign( { authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user.id diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 48f5a434e..c2c337292 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,5 +1,3 @@ -import jwt from "jsonwebtoken"; - import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -131,7 +129,7 @@ export const authSignupServiceFactory = ({ await userDAL.updateById(user.id, { isEmailVerified: true }); // generate jwt token this is a temporary token - const jwtToken = jwt.sign( + const jwtToken = crypto.jwt().sign( { authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user.id.toString() @@ -364,7 +362,7 @@ export const authSignupServiceFactory = ({ }); if (!tokenSession) throw new Error("Failed to create token"); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { authMethod: authMethod || AuthMethod.EMAIL, authTokenType: AuthTokenType.ACCESS_TOKEN, @@ -377,7 +375,7 @@ export const authSignupServiceFactory = ({ { expiresIn: tokenSessionExpiresIn } ); - const refreshToken = jwt.sign( + const refreshToken = crypto.jwt().sign( { authMethod: authMethod || AuthMethod.EMAIL, authTokenType: AuthTokenType.REFRESH_TOKEN, @@ -551,7 +549,7 @@ export const authSignupServiceFactory = ({ }); if (!tokenSession) throw new Error("Failed to create token"); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { authMethod: AuthMethod.EMAIL, authTokenType: AuthTokenType.ACCESS_TOKEN, @@ -563,7 +561,7 @@ export const authSignupServiceFactory = ({ { expiresIn: appCfg.JWT_SIGNUP_LIFETIME } ); - const refreshToken = jwt.sign( + const refreshToken = crypto.jwt().sign( { authMethod: AuthMethod.EMAIL, authTokenType: AuthTokenType.REFRESH_TOKEN, diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 0937b9640..e9863f666 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -1,7 +1,7 @@ -import jwt, { JwtPayload } from "jsonwebtoken"; - import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; +import { JWTPayload } from "@app/lib/crypto/cryptography/types"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; @@ -81,7 +81,7 @@ export const identityAccessTokenServiceFactory = ({ const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => { const appCfg = getConfig(); - const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as TIdentityAccessTokenJwtPayload; + const decodedToken = crypto.jwt().verify(accessToken, appCfg.AUTH_SECRET) as TIdentityAccessTokenJwtPayload; if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) { throw new BadRequestError({ message: "Only identity access tokens can be renewed" }); } @@ -145,7 +145,7 @@ export const identityAccessTokenServiceFactory = ({ expiresIn = undefined; } - const renewedToken = jwt.sign( + const renewedToken = crypto.jwt().sign( { identityId: decodedToken.identityId, clientSecretId: decodedToken.clientSecretId, @@ -162,7 +162,7 @@ export const identityAccessTokenServiceFactory = ({ const revokeAccessToken = async (accessToken: string) => { const appCfg = getConfig(); - const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & { + const decodedToken = crypto.jwt().verify(accessToken, appCfg.AUTH_SECRET) as JWTPayload & { identityAccessTokenId: string; }; if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) { diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index 77ae64121..819329a22 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -13,6 +12,7 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; @@ -103,7 +103,7 @@ export const identityAliCloudAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityAliCloudAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index b60366335..7c339f15e 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; import axios from "axios"; -import jwt from "jsonwebtoken"; import RE2 from "re2"; import { IdentityAuthMethod } from "@app/db/schemas"; @@ -13,6 +12,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -168,7 +168,7 @@ export const identityAwsAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityAwsAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts index 741d7e63c..cb06f3d40 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts @@ -1,6 +1,6 @@ import axios from "axios"; -import jwt from "jsonwebtoken"; +import { crypto } from "@app/lib/crypto"; import { UnauthorizedError } from "@app/lib/errors"; import { TAzureAuthJwtPayload, TAzureJwksUriResponse, TDecodedAzureAuthJwt } from "./identity-azure-auth-types"; @@ -16,7 +16,7 @@ export const validateAzureIdentity = async ({ }) => { const jwksUri = `https://login.microsoftonline.com/${tenantId}/discovery/keys`; - const decodedJwt = jwt.decode(azureJwt, { complete: true }) as TDecodedAzureAuthJwt; + const decodedJwt = crypto.jwt().decode(azureJwt, { complete: true }) as TDecodedAzureAuthJwt; const { kid } = decodedJwt.header; @@ -35,7 +35,7 @@ export const validateAzureIdentity = async ({ resource = resource.slice(0, -1); } - return jwt.verify(azureJwt, publicKey, { + return crypto.jwt().verify(azureJwt, publicKey, { audience: resource, issuer: `https://sts.windows.net/${tenantId}/` }) as TAzureAuthJwtPayload; diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 4c0d2164b..35103c8cf 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -1,5 +1,4 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -10,6 +9,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -96,7 +96,7 @@ export const identityAzureAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityAzureAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts index 05567d190..a089a24f6 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { OAuth2Client } from "google-auth-library"; -import jwt from "jsonwebtoken"; +import { crypto } from "@app/lib/crypto"; import { UnauthorizedError } from "@app/lib/errors"; import { TDecodedGcpIamAuthJwt, TGcpIdTokenPayload } from "./identity-gcp-auth-types"; @@ -48,7 +48,7 @@ export const validateIamIdentity = async ({ identityId: string; jwt: string; }) => { - const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt; + const decodedJwt = crypto.jwt().decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt; const { sub, aud } = decodedJwt.payload; const { @@ -61,7 +61,7 @@ export const validateIamIdentity = async ({ const publicKey = data[decodedJwt.header.kid]; - jwt.verify(serviceAccountJwt, publicKey, { + crypto.jwt().verify(serviceAccountJwt, publicKey, { algorithms: ["RS256"] }); diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 1e21d1d3e..b83697e52 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -1,5 +1,4 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -10,6 +9,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -135,7 +135,7 @@ export const identityGcpAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityGcpAuth.identityId, identityAccessTokenId: identityAccessToken.id, 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 index 7ebf75a85..546980f41 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -12,6 +12,8 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; +import { CompleteJWTPayload } from "@app/lib/crypto/cryptography/types"; import { BadRequestError, ForbiddenRequestError, @@ -79,7 +81,7 @@ export const identityJwtAuthServiceFactory = ({ orgId: identityMembershipOrg.orgId }); - const decodedToken = jwt.decode(jwtValue, { complete: true }); + const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }) as CompleteJWTPayload; if (!decodedToken) { throw new UnauthorizedError({ message: "Invalid JWT" @@ -106,11 +108,11 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { kid } = decodedToken.header; + const { kid } = decodedToken.header as { kid: string }; const jwtSigningKey = await client.getSigningKey(kid); try { - tokenData = jwt.verify(jwtValue, jwtSigningKey.getPublicKey()) as Record; + tokenData = crypto.jwt().verify(jwtValue, jwtSigningKey.getPublicKey()) as Record; } catch (error) { if (error instanceof jwt.JsonWebTokenError) { throw new UnauthorizedError({ @@ -129,7 +131,7 @@ export const identityJwtAuthServiceFactory = ({ let isMatchAnyKey = false; for (const publicKey of decryptedPublicKeys) { try { - tokenData = jwt.verify(jwtValue, publicKey) as Record; + tokenData = crypto.jwt().verify(jwtValue, publicKey) as Record; isMatchAnyKey = true; } catch (error) { if (error instanceof jwt.JsonWebTokenError) { @@ -225,7 +227,7 @@ export const identityJwtAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityJwtAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 3ce28361f..41491ab2f 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -1,7 +1,6 @@ import { ForbiddenError } from "@casl/ability"; import axios, { AxiosError } from "axios"; import https from "https"; -import jwt from "jsonwebtoken"; import RE2 from "re2"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; @@ -19,6 +18,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -396,7 +396,7 @@ export const identityKubernetesAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityKubernetesAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 89a169b1a..399ef7da9 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; @@ -12,6 +11,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -153,7 +153,7 @@ export const identityLdapAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityLdapAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index 106c30486..3b6450e6e 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import jwt from "jsonwebtoken"; import RE2 from "re2"; import { IdentityAuthMethod } from "@app/db/schemas"; @@ -14,6 +13,7 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; @@ -107,7 +107,7 @@ export const identityOciAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityOciAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 211d00163..997ed6dfd 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -13,6 +13,8 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; +import { CompleteJWTPayload } from "@app/lib/crypto/cryptography/types"; import { BadRequestError, ForbiddenRequestError, @@ -93,7 +95,7 @@ export const identityOidcAuthServiceFactory = ({ ); const jwksUri = discoveryDoc.jwks_uri; - const decodedToken = jwt.decode(oidcJwt, { complete: true }); + const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true }) as CompleteJWTPayload; if (!decodedToken) { throw new UnauthorizedError({ message: "Invalid JWT" @@ -105,12 +107,12 @@ export const identityOidcAuthServiceFactory = ({ requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined }); - const { kid } = decodedToken.header; + const { kid } = decodedToken.header as { kid: string }; const oidcSigningKey = await client.getSigningKey(kid); let tokenData: Record; try { - tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), { + tokenData = crypto.jwt().verify(oidcJwt, oidcSigningKey.getPublicKey(), { issuer: identityOidcAuth.boundIssuer }) as Record; } catch (error) { @@ -193,7 +195,7 @@ export const identityOidcAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityOidcAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index e2c2e4cb8..e47cc4c9a 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -1,5 +1,4 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -135,7 +134,7 @@ export const identityTlsCertAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityTlsCertAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 8f1218045..82949090e 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -1,5 +1,4 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -10,6 +9,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -362,7 +362,7 @@ export const identityTokenAuthServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityTokenAuth.identityId, identityAccessTokenId: identityAccessToken.id, diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index c159f7364..643f32047 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -1,5 +1,4 @@ import { ForbiddenError } from "@casl/ability"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -147,7 +146,7 @@ export const identityUaServiceFactory = ({ }); const appCfg = getConfig(); - const accessToken = jwt.sign( + const accessToken = crypto.jwt().sign( { identityId: identityUa.identityId, clientSecretId: validClientSecretInfo.id, diff --git a/backend/src/services/integration-auth/integration-token.ts b/backend/src/services/integration-auth/integration-token.ts index a15c9dd1f..1f7061076 100644 --- a/backend/src/services/integration-auth/integration-token.ts +++ b/backend/src/services/integration-auth/integration-token.ts @@ -1,7 +1,6 @@ -import jwt from "jsonwebtoken"; - import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { Integrations, IntegrationUrls } from "./integration-list"; @@ -718,7 +717,7 @@ const exchangeRefreshGCPSecretManager = async ({ exp: Math.floor(Date.now() / 1000) + 3600 }; - const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" }); + const token = crypto.jwt().sign(payload, serviceAccount.private_key, { algorithm: "RS256" }); const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } = await request.post( IntegrationUrls.GCP_TOKEN_URL, diff --git a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts index d477143a9..8734b8459 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts @@ -1,11 +1,11 @@ /* eslint-disable class-methods-use-this */ import axios from "axios"; import { TeamsActivityHandler, TurnContext } from "botbuilder"; -import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types"; @@ -71,7 +71,7 @@ export const verifyTenantFromCode = async ( ); // Verify application token - const { tid: tenantIdFromApplicationAccessToken } = jwt.decode(applicationAccessToken) as { tid: string }; + const { tid: tenantIdFromApplicationAccessToken } = crypto.jwt().decode(applicationAccessToken) as { tid: string }; if (tenantIdFromApplicationAccessToken !== tenantId) { throw new BadRequestError({ @@ -80,7 +80,9 @@ export const verifyTenantFromCode = async ( } // Verify user authorization token - const { tid: tenantIdFromAuthorizationAccessToken } = jwt.decode(authorizationAccessToken) as { tid: string }; + const { tid: tenantIdFromAuthorizationAccessToken } = crypto.jwt().decode(authorizationAccessToken) as { + tid: string; + }; if (tenantIdFromAuthorizationAccessToken !== tenantId) { throw new BadRequestError({ diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 4bd8d1715..eb712843a 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -1,6 +1,5 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { @@ -596,7 +595,7 @@ export const orgServiceFactory = ({ const cfg = getConfig(); const authToken = authorizationHeader.replace("Bearer ", ""); - const decodedToken = jwt.verify(authToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload; + const decodedToken = crypto.jwt().verify(authToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload; if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" }); const response = await orgDAL.transaction(async (tx) => { @@ -1304,7 +1303,7 @@ export const orgServiceFactory = ({ } const appCfg = getConfig(); - const token = jwt.sign( + const token = crypto.jwt().sign( { authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user.id diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 70cf7d67f..0de4950f0 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,5 +1,4 @@ import { CronJob } from "cron"; -import jwt from "jsonwebtoken"; import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -670,7 +669,7 @@ export const superAdminServiceFactory = ({ tx ); - const generatedAccessToken = jwt.sign( + const generatedAccessToken = crypto.jwt().sign( { identityId: newIdentity.id, identityAccessTokenId: newToken.id,