From 8288301e91d1d28cf9835c94520fb705254109da Mon Sep 17 00:00:00 2001 From: arielpetit Date: Mon, 4 Nov 2024 12:26:00 +0100 Subject: [PATCH 1/4] Send JWT as part of auth header in request --- .../__tests__/apiSercice.test.ts | 65 +++++++++++++++++++ src/services/keyManagement/apiService.ts | 27 ++++++++ src/services/keyManagement/registerService.ts | 5 +- 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/services/keyManagement/__tests__/apiSercice.test.ts create mode 100644 src/services/keyManagement/apiService.ts diff --git a/src/services/keyManagement/__tests__/apiSercice.test.ts b/src/services/keyManagement/__tests__/apiSercice.test.ts new file mode 100644 index 0000000..99bfd6b --- /dev/null +++ b/src/services/keyManagement/__tests__/apiSercice.test.ts @@ -0,0 +1,65 @@ +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import { sendOTP } from "../apiService"; + +describe("sendOTP", () => { + let mock: MockAdapter; // Explicitly declare the type + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it("should send OTP successfully with valid inputs", async () => { + const fullPhoneNumber = "1234567890"; + const jwtToken = "valid-token"; + + mock + .onPost("http://localhost:8080/api/registration") + .reply(200, { message: "OTP sent" }); + + const response = await sendOTP(fullPhoneNumber, jwtToken); + + expect(response).toEqual({ message: "OTP sent" }); + }); + + it("should throw an error when API responds with an error", async () => { + const fullPhoneNumber = "1234567890"; + const jwtToken = "valid-token"; + + mock.onPost("http://localhost:8080/api/registration").reply(500); + + await expect(sendOTP(fullPhoneNumber, jwtToken)).rejects.toThrow( + "Failed to send OTP", + ); + }); + + it("should throw an error when an invalid JWT token is provided", async () => { + const fullPhoneNumber = "1234567890"; + const jwtToken = "invalid-token"; + + mock.onPost("http://localhost:8080/api/registration").reply(401); + + await expect(sendOTP(fullPhoneNumber, jwtToken)).rejects.toThrow( + "Failed to send OTP", + ); + }); + + it("should handle empty phone number and JWT token", async () => { + await expect(sendOTP("", "")).rejects.toThrow("Failed to send OTP"); + }); + + it("should handle network error", async () => { + const fullPhoneNumber = "1234567890"; + const jwtToken = "valid-token"; + + mock.onPost("http://localhost:8080/api/registration").networkError(); + + await expect(sendOTP(fullPhoneNumber, jwtToken)).rejects.toThrow( + "Failed to send OTP", + ); + }); +}); diff --git a/src/services/keyManagement/apiService.ts b/src/services/keyManagement/apiService.ts new file mode 100644 index 0000000..3f129b1 --- /dev/null +++ b/src/services/keyManagement/apiService.ts @@ -0,0 +1,27 @@ +import axios from "axios"; +export const sendOTP = async (fullPhoneNumber: string, jwtToken: string) => { + // Create the request object with both phone number and public key + const requestBody = { + phoneNumber: fullPhoneNumber, + }; + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${jwtToken}`, + }; + + try { + const response = await axios.post( + "http://localhost:8080/api/registration", + requestBody, + { headers }, + ); + + // Log the response from the backend + console.log("Response from backend:", response.data); + + return response.data; + } catch (error) { + console.error("Error sending OTP:", error); + throw new Error("Failed to send OTP"); + } +}; diff --git a/src/services/keyManagement/registerService.ts b/src/services/keyManagement/registerService.ts index ff19eb2..9ec7fff 100644 --- a/src/services/keyManagement/registerService.ts +++ b/src/services/keyManagement/registerService.ts @@ -1,6 +1,7 @@ import { generateJWT } from "./jwtService"; import storeKeyPair, { retrieveKeyPair } from "./storeKey"; import checkKeyPairExists from "./checkKeyPairExists"; +import { sendOTP } from "./apiService"; export async function sendOtpWithKeyManagement( phoneNumber: string, @@ -21,7 +22,9 @@ export async function sendOtpWithKeyManagement( // Generate JWT with the full phone number jwtToken = await generateJWT(phoneNumber, privateKey); - console.log("Generated JWT:", jwtToken); + + // Send the JWT and phone number + await sendOTP(phoneNumber, jwtToken); } else { console.log("Key pair already exists. Skipping generation."); } From 7e2e2d4d57329d92e5a1f99a6a49bf26a6a55bc4 Mon Sep 17 00:00:00 2001 From: arielpetit Date: Mon, 4 Nov 2024 15:21:27 +0100 Subject: [PATCH 2/4] fixed test using happy-dom as testing environment --- jest.config.ts | 14 -------- jest.setup.ts | 12 ------- package-lock.json | 36 +++++++++++++++++++ package.json | 1 + .../keyManagement/__tests__/storeKey.test.ts | 2 +- src/services/keyManagement/storageSetup.ts | 1 + src/services/keyManagement/storeKey.ts | 4 +-- vitest.config.ts | 2 +- 8 files changed, 42 insertions(+), 30 deletions(-) delete mode 100644 jest.config.ts delete mode 100644 jest.setup.ts diff --git a/jest.config.ts b/jest.config.ts deleted file mode 100644 index dd48bba..0000000 --- a/jest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -// jest.config.ts -import type { Config } from 'jest'; - -const config: Config = { - preset: 'ts-jest', - testEnvironment: 'jsdom', // Use jsdom for DOM-related tests - setupFiles: ["/jest.setup.js"], // Ensure this points to your setup file - transform: { - "^.+\\.tsx?$": "ts-jest", - }, - moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'], -}; - -export default config; diff --git a/jest.setup.ts b/jest.setup.ts deleted file mode 100644 index dc2bc3e..0000000 --- a/jest.setup.ts +++ /dev/null @@ -1,12 +0,0 @@ -// jest.setup.js -global.indexedDB = { - open: jest.fn(() => ({ - onsuccess: jest.fn(), - onerror: jest.fn(), - result: { - createObjectStore: jest.fn(), - transaction: jest.fn(), - }, - })), - }; - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e05d81b..c8e39c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.6", "fake-indexeddb": "^6.0.0", + "happy-dom": "^15.8.3", "jsdom": "^25.0.1", "postcss": "^8.4.47", "prettier": "^3.3.3", @@ -6524,6 +6525,41 @@ "dev": true, "license": "MIT" }, + "node_modules/happy-dom": { + "version": "15.8.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.8.3.tgz", + "integrity": "sha512-YR9nUWN/T2bH7pPLEYMhTp4DQExPH+mC4KulJDgimCb+FY3Er0Vp6SOOcBXrNfMTri3lAk9uSZqUTG2hgZOYwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0", + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/happy-dom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/happy-dom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", diff --git a/package.json b/package.json index ebeba1d..79ccd50 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.6", "fake-indexeddb": "^6.0.0", + "happy-dom": "^15.8.3", "jsdom": "^25.0.1", "postcss": "^8.4.47", "prettier": "^3.3.3", diff --git a/src/services/keyManagement/__tests__/storeKey.test.ts b/src/services/keyManagement/__tests__/storeKey.test.ts index 598c4a5..e5193b2 100644 --- a/src/services/keyManagement/__tests__/storeKey.test.ts +++ b/src/services/keyManagement/__tests__/storeKey.test.ts @@ -29,4 +29,4 @@ describe("Key Pair Storage Tests", () => { expect(retrievedKeys.publicKey).toBeNull(); // Expect public key to be null expect(retrievedKeys.privateKey).toBeNull(); // Expect private key to be null }); -}); +}); \ No newline at end of file diff --git a/src/services/keyManagement/storageSetup.ts b/src/services/keyManagement/storageSetup.ts index 34aaf40..2b8771e 100644 --- a/src/services/keyManagement/storageSetup.ts +++ b/src/services/keyManagement/storageSetup.ts @@ -20,6 +20,7 @@ const storage: StorageFactory = new StorageFactory( if (!db.objectStoreNames.contains("keys")) { db.createObjectStore("keys", { keyPath: "kid", + autoIncrement: true, }); } }, diff --git a/src/services/keyManagement/storeKey.ts b/src/services/keyManagement/storeKey.ts index 846d088..5de8c45 100644 --- a/src/services/keyManagement/storeKey.ts +++ b/src/services/keyManagement/storeKey.ts @@ -1,5 +1,5 @@ -import storage from "./storageSetup"; -import generateKeyPair from "./generateKey"; +import storage from "./storageSetup"; // Import the initialized storage +import generateKeyPair from "./generateKey"; // Import your existing key generation function // Function to store a key pair in IndexedDB export async function storeKeyPair() { diff --git a/vitest.config.ts b/vitest.config.ts index bdaf127..fda341d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, - environment: "jsdom", + environment: "happy-dom", coverage: { provider: "v8", reporter: ["text", "json", "html"], From b5bba2b8af3cb89274e65df9f5bc0e513f37132e Mon Sep 17 00:00:00 2001 From: arielpetit Date: Mon, 4 Nov 2024 15:22:10 +0100 Subject: [PATCH 3/4] fixed test using happy-dom as testing environment --- src/services/keyManagement/__tests__/storeKey.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/keyManagement/__tests__/storeKey.test.ts b/src/services/keyManagement/__tests__/storeKey.test.ts index e5193b2..598c4a5 100644 --- a/src/services/keyManagement/__tests__/storeKey.test.ts +++ b/src/services/keyManagement/__tests__/storeKey.test.ts @@ -29,4 +29,4 @@ describe("Key Pair Storage Tests", () => { expect(retrievedKeys.publicKey).toBeNull(); // Expect public key to be null expect(retrievedKeys.privateKey).toBeNull(); // Expect private key to be null }); -}); \ No newline at end of file +}); From 59a085cd76e6b598d8f2ba75f787eca5ebf8639b Mon Sep 17 00:00:00 2001 From: Menkene Koufan Date: Tue, 5 Nov 2024 13:17:18 +0100 Subject: [PATCH 4/4] added public key to protected header of http request --- src/services/keyManagement/__tests__/jwtservice.test.ts | 6 ++++-- src/services/keyManagement/jwtService.ts | 7 ++++++- src/services/keyManagement/registerService.ts | 2 +- src/services/keyManagement/storeKey.ts | 3 +-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/services/keyManagement/__tests__/jwtservice.test.ts b/src/services/keyManagement/__tests__/jwtservice.test.ts index 13c056c..46155fe 100644 --- a/src/services/keyManagement/__tests__/jwtservice.test.ts +++ b/src/services/keyManagement/__tests__/jwtservice.test.ts @@ -17,11 +17,13 @@ describe("JWT Generation", () => { // Convert privateKey to JWK for use with generateJWT const privateKeyJWK = await jose.exportJWK(privateKey); + // Convert publicKey to JWK for verification + const publicKeyJWK = await jose.exportJWK(publicKey); + // Step 3: Call the generateJWT function - const jwt = await generateJWT(data, privateKeyJWK); + const jwt = await generateJWT(data, privateKeyJWK, publicKeyJWK); // Step 4: Verify the JWT signature and payload - const publicKeyJWK = await jose.exportJWK(publicKey); // Convert publicKey to JWK for verification const { payload } = await jose.jwtVerify( jwt, await jose.importJWK(publicKeyJWK, "ES256"), diff --git a/src/services/keyManagement/jwtService.ts b/src/services/keyManagement/jwtService.ts index ca763a9..875e56e 100644 --- a/src/services/keyManagement/jwtService.ts +++ b/src/services/keyManagement/jwtService.ts @@ -8,6 +8,7 @@ function hashPayload(payload: string): string { export async function generateJWT( data: string, privateKeyJWK: jose.JWK, + publicKeyJWK: jose.JWK, ): Promise { // Hash the payload const hashedPayload = hashPayload(data); @@ -23,7 +24,11 @@ export async function generateJWT( // Sign the JWT with the private key const jwt = await new jose.SignJWT(jwtPayload) - .setProtectedHeader({ alg: "ES256" }) + .setProtectedHeader({ + typ: "JWT", + alg: "ES256", + jwk: publicKeyJWK, + }) .sign(privateKey); return jwt; diff --git a/src/services/keyManagement/registerService.ts b/src/services/keyManagement/registerService.ts index 9ec7fff..f75203d 100644 --- a/src/services/keyManagement/registerService.ts +++ b/src/services/keyManagement/registerService.ts @@ -21,7 +21,7 @@ export async function sendOtpWithKeyManagement( } // Generate JWT with the full phone number - jwtToken = await generateJWT(phoneNumber, privateKey); + jwtToken = await generateJWT(phoneNumber, privateKey, publicKey); // Send the JWT and phone number await sendOTP(phoneNumber, jwtToken); diff --git a/src/services/keyManagement/storeKey.ts b/src/services/keyManagement/storeKey.ts index 5de8c45..8182cfe 100644 --- a/src/services/keyManagement/storeKey.ts +++ b/src/services/keyManagement/storeKey.ts @@ -3,12 +3,11 @@ import generateKeyPair from "./generateKey"; // Import your existing key generat // Function to store a key pair in IndexedDB export async function storeKeyPair() { - const { publicKey, privateKey, kid } = await generateKeyPair(); + const { publicKey, privateKey } = await generateKeyPair(); // Store both keys in a single record in IndexedDB await storage.insert("keys", { value: { - id: kid, pub: { ...publicKey }, priv: { ...privateKey }, },