diff --git a/backend-pg/e2e-test/routes/v1/login.spec.ts b/backend-pg/e2e-test/routes/v1/login.spec.ts new file mode 100644 index 000000000..d27934ccc --- /dev/null +++ b/backend-pg/e2e-test/routes/v1/login.spec.ts @@ -0,0 +1,45 @@ +import { testUser } from "@app/db/seeds/1-user"; +import jsrp from "jsrp"; + +describe("Login V1 Router", async () => { + // eslint-disable-next-line + const client = new jsrp.client(); + await new Promise((resolve) => { + client.init({ username: testUser.email, password: testUser.password }, () => resolve(null)); + }); + let clientProof: string; + + test("Login first phase", async () => { + const res = await testServer.inject({ + method: "POST", + url: "/api/v3/auth/login1", + body: { + email: "test@localhost.local", + clientPublicKey: client.getPublicKey() + } + }); + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("serverPublicKey"); + expect(payload).toHaveProperty("salt"); + client.setSalt(payload.salt); + client.setServerPublicKey(payload.serverPublicKey); + clientProof = client.getProof(); // called M1 + }); + + test("Login second phase", async () => { + const res = await testServer.inject({ + method: "POST", + url: "/api/v3/auth/login2", + body: { + email: testUser.email, + clientProof + } + }); + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("mfaEnabled"); + expect(payload).toHaveProperty("token"); + expect(payload.mfaEnabled).toBeFalsy(); + }); +}); diff --git a/backend-pg/e2e-test/routes/v1/test.spec.ts b/backend-pg/e2e-test/routes/v1/test.spec.ts new file mode 100644 index 000000000..fefb51ad3 --- /dev/null +++ b/backend-pg/e2e-test/routes/v1/test.spec.ts @@ -0,0 +1,9 @@ +describe("Status V1 Router", async () => { + test("Simple check", async () => { + const res = await testServer.inject({ + method: "GET", + url: "/api/status" + }); + expect(res.statusCode).toBe(200); + }); +}); diff --git a/backend-pg/e2e-test/vitest-environment-knex.ts b/backend-pg/e2e-test/vitest-environment-knex.ts index 1f98d787b..f20a09404 100644 --- a/backend-pg/e2e-test/vitest-environment-knex.ts +++ b/backend-pg/e2e-test/vitest-environment-knex.ts @@ -20,7 +20,8 @@ export default { connection: process.env.DB_CONNECTION_URI, migrations: { directory: path.join(__dirname, "../src/db/migrations"), - extension: "ts" + extension: "ts", + tableName: "infisical_migrations" }, seeds: { directory: path.join(__dirname, "../src/db/seeds"), diff --git a/backend-pg/package-lock.json b/backend-pg/package-lock.json index 6d4b06dcd..a282ac60e 100644 --- a/backend-pg/package-lock.json +++ b/backend-pg/package-lock.json @@ -89,6 +89,7 @@ "prompt-sync": "^4.2.0", "rimraf": "^5.0.5", "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", "tsup": "^8.0.1", "tsx": "^4.4.0", "typescript": "^5.3.2", @@ -8175,6 +8176,18 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -11986,6 +11999,20 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", diff --git a/backend-pg/package.json b/backend-pg/package.json index 4f4fe52ac..db4d2ca2a 100644 --- a/backend-pg/package.json +++ b/backend-pg/package.json @@ -58,6 +58,7 @@ "prompt-sync": "^4.2.0", "rimraf": "^5.0.5", "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", "tsup": "^8.0.1", "tsx": "^4.4.0", "typescript": "^5.3.2", diff --git a/backend-pg/src/db/seeds/1-user.ts b/backend-pg/src/db/seeds/1-user.ts index 6ca3caf17..696eca198 100644 --- a/backend-pg/src/db/seeds/1-user.ts +++ b/backend-pg/src/db/seeds/1-user.ts @@ -1,10 +1,78 @@ import { Knex } from "knex"; - +import jsrp from "jsrp"; +import nacl from "tweetnacl"; +import { encodeBase64 } from "tweetnacl-util"; +import argon2, { argon2id } from "argon2"; import { AuthMethod } from "../../services/auth/auth-type"; import { TableName } from "../schemas"; +import crypto from "node:crypto"; +import { encryptSymmetric } from "@app/lib/crypto"; export const testUser = { - email: "test@localhost.local" + email: "test@localhost.local", + password: process.env.TEST_USER_PASSWORD || "testInfisical@1" +}; + +export const generateUserSrpKeys = async (password: string) => { + const pair = nacl.box.keyPair(); + const secretKeyUint8Array = pair.secretKey; + const publicKeyUint8Array = pair.publicKey; + const privateKey = encodeBase64(secretKeyUint8Array); + const publicKey = encodeBase64(publicKeyUint8Array); + + // eslint-disable-next-line + const client = new jsrp.client(); + await new Promise((resolve) => { + client.init({ username: testUser.email, password: testUser.password }, () => resolve(null)); + }); + const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>( + (resolve, reject) => { + client.createVerifier((err, res) => { + if (err) return reject(err); + return resolve(res); + }); + } + ); + const derivedKey = await argon2.hash(password, { + salt: Buffer.from(salt), + memoryCost: 65536, + timeCost: 3, + parallelism: 1, + hashLength: 32, + type: argon2id, + raw: true + }); + if (!derivedKey) throw new Error("Failed to derive key from password"); + + const key = crypto.randomBytes(32); + + // create encrypted private key by encrypting the private + // key with the symmetric key [key] + const { + ciphertext: encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + } = encryptSymmetric(privateKey, key.toString("base64")); + + // create the protected key by encrypting the symmetric key + // [key] with the derived key + const { + ciphertext: protectedKey, + iv: protectedKeyIV, + tag: protectedKeyTag + } = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64")); + + return { + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + }; }; export async function seed(knex: Knex): Promise { @@ -17,7 +85,7 @@ export async function seed(knex: Knex): Promise { const [user] = await knex(TableName.Users) .insert([ { - email: "test@localhost.local", + email: testUser.email, superAdmin: true, firstName: "test", lastName: "", @@ -30,21 +98,20 @@ export async function seed(knex: Knex): Promise { ]) .returning("*"); + const encKeys = await generateUserSrpKeys(testUser.password); // password: testInfisical@1 await knex(TableName.UserEncryptionKey).insert([ { encryptionVersion: 2, - protectedKey: - "Ng0qHLdRdoLR4lHS0xsJVYLR6Y9F44MjkC9dKz1AcGMcoN1VuXjCMySshLPAj2Fboyz9Jo7Qc72YLTaHUGaubA==", - protectedKeyIV: "ou9NrOxwQTYJUdIMrQDuPQ==", - protectedKeyTag: "BZQMY7mE14maBKzPZpCgtQ==", - publicKey: "rH+riApRZX6HkHMBOhyDhnUBzWWMOx/EBx4gnHYRUTs=", - encryptedPrivateKey: "aNnCmeG6sWh7qF40QsFQx9wfgpRTMLtnrtZ+DgkvNXOPYaPW1n2YnH+g20k=", - iv: "Q4O3elA0iwUvsgSOW0rIkA==", - tag: "RCuZe9paDKS71hluuX7qbw==", - salt: "faac495e264903a7cd42bd75298ddecbf27824f11a744a26788e20b5421ec7e0", - verifier: - "e1f9aff68b4e2c55f21652527bcb070dd171ea2b8de8411890ad13dd7e2da965f29cd5f43a7786690764c3f6233c32c5f98c8da89cd2869bdf940121fff3c25888caa05d8d76c8bb88ddb9c706f2a8f336778c24f64d70f498bd154ac4bf9d79241ed20b343c1fa6d8061cc834401ef89df615bb6e66388dafafad57ed5b334899846d2f031d6134a6681b0cf9ca5ccd13aac53992f564e5550b26222b41ef9c3b6c4e3b8c0ee654762c8feb8a450d4eb9154f7d21b6b409d1027dc298b3240fb7795bae96415dccac689ba705efb82bae469107832bffc703abf6ec532930241061bb88bca3df2ee187ebe06a8f79ccfc851a5f90a83d5703b9ebfc0d08b9ad626870cad09b2ba7ba82c5297f242f5a012bb92c137190fd0168f83f24b62a01431a796b2aba2e133a71106e4271a07c5bdbeeddce5680f785371f5388941e736140b275d6df79cf9f8ce99b6575f7ea4739e7b6fba2f46e53e4bf8e01ca16aecb1138c218fb29b33473515008917f64067ae4104f9eac9abca193ee7596e70de1277a1c789af883d297eb6e72d106ec9899e32d716843ecb8295d1ec54d67b6a754ff5db75ae8233d6472106a7eefb0f9d0b62fe1e2278ec8c4e52e9c65bf2ec2c4e9a20f63c839ce36e67bb5519ef2b425399bad03b47aa0cb5b369444bb13e20d15548da348d6ab852600c35af36f3df8dd1bfc7fa2c00ebf68bc3033da00", + protectedKey: encKeys.protectedKey, + protectedKeyIV: encKeys.protectedKeyIV, + protectedKeyTag: encKeys.protectedKeyTag, + publicKey: encKeys.publicKey, + encryptedPrivateKey: encKeys.encryptedPrivateKey, + iv: encKeys.encryptedPrivateKeyIV, + tag: encKeys.encryptedPrivateKeyTag, + salt: encKeys.salt, + verifier: encKeys.verifier, userId: user.id } ]); diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts index 4e1da0b8d..195c0fe07 100644 --- a/backend-pg/src/server/routes/index.ts +++ b/backend-pg/src/server/routes/index.ts @@ -472,7 +472,7 @@ export const registerRoutes = async ( await server.register(injectAuditLogInfo); server.route({ - url: "/status", + url: "/api/status", method: "GET", schema: { response: { diff --git a/backend-pg/src/server/routes/v1/admin-router.ts b/backend-pg/src/server/routes/v1/admin-router.ts index 945441dca..bc500d98e 100644 --- a/backend-pg/src/server/routes/v1/admin-router.ts +++ b/backend-pg/src/server/routes/v1/admin-router.ts @@ -37,9 +37,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }) } }, - preHandler: (req) => { + onRequest: (req, _, done) => { verifyAuth([AuthMode.JWT, AuthMode.API_KEY])(req); verifySuperAdmin(req); + done(); }, handler: async (req) => { const config = await server.services.superAdmin.updateServerCfg(req.body); diff --git a/backend-pg/src/services/super-admin/super-admin-service.ts b/backend-pg/src/services/super-admin/super-admin-service.ts index b2a0d570d..325008a4f 100644 --- a/backend-pg/src/services/super-admin/super-admin-service.ts +++ b/backend-pg/src/services/super-admin/super-admin-service.ts @@ -24,7 +24,7 @@ export const superAdminServiceFactory = ({ const initServerCfg = async () => { serverCfg = await serverCfgDal.findOne({}); if (!serverCfg) { - const newCfg = await serverCfgDal.create({ initialized: true, allowSignUp: true }); + const newCfg = await serverCfgDal.create({ initialized: false, allowSignUp: true }); serverCfg = newCfg; return newCfg; } @@ -39,6 +39,7 @@ export const superAdminServiceFactory = ({ const updateServerCfg = async (data: TSuperAdminUpdate) => { const cfg = await serverCfgDal.updateById(serverCfg.id, data); + serverCfg = cfg; return cfg; }; @@ -59,7 +60,7 @@ export const superAdminServiceFactory = ({ userAgent }: TAdminSignUpDTO) => { const existingUser = await userDal.findOne({ email }); - if (!existingUser) + if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" }); const userInfo = await userDal.transaction(async (tx) => { @@ -68,7 +69,8 @@ export const superAdminServiceFactory = ({ firstName, lastName, email, - superAdmin: true + superAdmin: true, + isAccepted: true }, tx ); diff --git a/backend-pg/tsconfig.json b/backend-pg/tsconfig.json index 4f9cf4dd6..e901076cf 100644 --- a/backend-pg/tsconfig.json +++ b/backend-pg/tsconfig.json @@ -1,23 +1,19 @@ { + "ts-node": { + // Do not forget to `npm i -D tsconfig-paths` + "require": ["tsconfig-paths/register"] + }, "compilerOptions": { "target": "esnext", "module": "commonjs", "allowJs": true, "resolveJsonModule": true, - "types": [ - "vitest/globals" - ], - "typeRoots": [ - "./node_modules/@types", - "./src/@types", - "./node_modules" - ], + "types": ["vitest/globals"], + "typeRoots": ["./node_modules/@types", "./src/@types", "./node_modules"], "sourceMap": true, "outDir": "dist", "strict": true, - "lib": [ - "esnext" - ], + "lib": ["esnext"], "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "experimentalDecorators": true, @@ -26,23 +22,11 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@app/*": [ - "./src/*" - ], - "@lib/*": [ - "./src/lib/*" - ], - "@server/*": [ - "./src/server/*" - ] + "@app/*": ["./src/*"], + "@lib/*": ["./src/lib/*"], + "@server/*": ["./src/server/*"] } }, - "include": [ - "src/**/*", - "scripts/**/*", - "e2e-test/**/*" - ], - "exclude": [ - "node_modules" - ] + "include": ["src/**/*", "scripts/**/*", "e2e-test/**/*"], + "exclude": ["node_modules"] } diff --git a/backend-pg/vitest.e2e.config.ts b/backend-pg/vitest.e2e.config.ts index 0948a86d8..e8636d405 100644 --- a/backend-pg/vitest.e2e.config.ts +++ b/backend-pg/vitest.e2e.config.ts @@ -5,7 +5,13 @@ export default defineConfig({ test: { globals: true, environment: "./e2e-test/vitest-environment-knex.ts", - include: ["./e2e-test/**/*.spec.ts"] + include: ["./e2e-test/**/*.spec.ts"], + poolOptions: { + threads: { + singleThread: true, + useAtomics: true + } + } }, plugins: [tsconfigPaths()] // only if you are using custom tsconfig paths, });