diff --git a/src/features/common/models/encoder-result.model.ts b/src/features/common/models/encoder-result.model.ts new file mode 100644 index 00000000..88221a08 --- /dev/null +++ b/src/features/common/models/encoder-result.model.ts @@ -0,0 +1,4 @@ +export interface EncoderResult { + jwt: string; + signingErrors: string[] | null; +} diff --git a/src/features/encoder/services/token-encoder.service.ts b/src/features/encoder/services/token-encoder.service.ts index 44ceaf93..e35044c5 100644 --- a/src/features/encoder/services/token-encoder.service.ts +++ b/src/features/encoder/services/token-encoder.service.ts @@ -36,6 +36,7 @@ import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-k import { useDebuggerStore } from "@/features/debugger/services/debugger.store"; import { SigningAlgCategoryValues } from "@/features/common/values/signing-alg-category.values"; import { EncoderInputsModel } from "@/features/debugger/models/encoder-inputs.model"; +import { EncoderResult } from "@/features/common/models/encoder-result.model"; type EncodingHeaderErrors = { headerErrors: string[] | null; @@ -183,7 +184,8 @@ class _TokenEncoderService { } if (encodeJWTResult.isOk()) { - stateUpdate.jwt = encodeJWTResult.value.trim(); + stateUpdate.jwt = encodeJWTResult.value.jwt.trim(); + stateUpdate.signingErrors = encodeJWTResult.value.signingErrors; } return { @@ -214,7 +216,7 @@ class _TokenEncoderService { } if (encodeJWTResult.isOk()) { - stateUpdate.jwt = encodeJWTResult.value.trim(); + stateUpdate.jwt = encodeJWTResult.value.jwt.trim(); useDebuggerStore.getState().setStash$({ asymmetricPublicKey: digitallySignedToken.publicKey, @@ -379,7 +381,7 @@ class _TokenEncoderService { } if (encodeJWTResult.isOk()) { - stateUpdate.jwt = encodeJWTResult.value.trim(); + stateUpdate.jwt = encodeJWTResult.value.jwt.trim(); } return { @@ -409,7 +411,7 @@ class _TokenEncoderService { } if (encodeJWTResult.isOk()) { - stateUpdate.jwt = encodeJWTResult.value.trim(); + stateUpdate.jwt = encodeJWTResult.value.jwt.trim(); } return { @@ -484,48 +486,61 @@ class _TokenEncoderService { payload: DecodedJwtPayloadModel, key: string, encodingFormat: EncodingValues, - ): Promise> { - if (isHmacAlg(header.alg)) { - if (!key) { - return err({ - task: DebuggerTaskValues.ENCODE, - input: DebuggerInputValues.KEY, - message: "Secret must not be empty.", - }); - } + ): Promise> { + if (!isHmacAlg(header.alg)) { + return err({ + task: DebuggerTaskValues.ENCODE, + input: DebuggerInputValues.HEADER, + message: `Invalid MAC algorithm. Only use MAC "alg" parameter values in the header as defined by [RFC 7518 (JSON Web Algorithms)](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1).`, + }); + } - const getAlgSizeResult = getAlgSize(header.alg); + if (!key) { + return err({ + task: DebuggerTaskValues.ENCODE, + input: DebuggerInputValues.KEY, + message: "Secret must not be empty.", + }); + } - if (getAlgSizeResult.isErr()) { - return err({ - task: DebuggerTaskValues.ENCODE, - input: DebuggerInputValues.KEY, - message: getAlgSizeResult.error, - }); - } + const getAlgSizeResult = getAlgSize(header.alg); - const checkHmacSecretLengthResult = checkHmacSecretLength( - key, - getAlgSizeResult.value.size, - encodingFormat, - ); + if (getAlgSizeResult.isErr()) { + return err({ + task: DebuggerTaskValues.ENCODE, + input: DebuggerInputValues.KEY, + message: getAlgSizeResult.error, + }); + } - if (checkHmacSecretLengthResult.isErr()) { - return err(checkHmacSecretLengthResult.error); - } + const checkHmacSecretLengthResult = checkHmacSecretLength( + key, + getAlgSizeResult.value.size, + encodingFormat, + ); - return await signWithSymmetricSecretKey( - header as CompactJWSHeaderParameters, - payload, - key, - encodingFormat, - ); + const signingError = checkHmacSecretLengthResult.isErr() + ? [checkHmacSecretLengthResult.error.message] + : null; + + const signWithSymmetricSecretKeyResult = await signWithSymmetricSecretKey( + header as CompactJWSHeaderParameters, + payload, + key, + encodingFormat, + ); + + if (signWithSymmetricSecretKeyResult.isErr()) { + return err({ + task: DebuggerTaskValues.ENCODE, + input: DebuggerInputValues.KEY, + message: signWithSymmetricSecretKeyResult.error.message, + }); } - return err({ - task: DebuggerTaskValues.ENCODE, - input: DebuggerInputValues.HEADER, - message: `Invalid MAC algorithm. Only use MAC "alg" parameter values in the header as defined by [RFC 7518 (JSON Web Algorithms)](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1).`, + return ok({ + jwt: signWithSymmetricSecretKeyResult.value, + signingErrors: signingError, }); } @@ -534,7 +549,7 @@ class _TokenEncoderService { payload: DecodedJwtPayloadModel, key: string, keyFormat: AsymmetricKeyFormatValues, - ): Promise> { + ): Promise> { if (isDigitalSignatureAlg(header.alg)) { if (!key) { return err({ @@ -544,12 +559,25 @@ class _TokenEncoderService { }); } - return await signWithAsymmetricPrivateKey( + const jwt = await signWithAsymmetricPrivateKey( header as CompactJWSHeaderParameters, payload, key, keyFormat, ); + + if (jwt.isErr()) { + return err({ + task: DebuggerTaskValues.ENCODE, + input: DebuggerInputValues.KEY, + message: "Private key must not be empty.", + }) + } + + return ok({ + jwt: jwt.value, + signingErrors: null, + }); } return err({ @@ -684,9 +712,7 @@ class _TokenEncoderService { symmetricSecretKeyEncoding: EncodingValues; }): Promise< Result< - { - jwt: string; - }, + EncoderResult, EncodingSymmetricSecretKeyErrors > > { @@ -767,6 +793,7 @@ class _TokenEncoderService { return ok({ jwt: encodeJwtResult.value.jwt.trim(), + signingErrors: encodeJwtResult.value.signingErrors, }); } @@ -861,9 +888,7 @@ class _TokenEncoderService { }, ): Promise< Result< - { - jwt: string; - }, + EncoderResult, EncodingJwtErrors > > { @@ -871,7 +896,7 @@ class _TokenEncoderService { const header = params.header; const payload = params.payload; - let encodeJWTResult: Result | null = null; + let encodeJWTResult: Result | null = null; if (algType === SigningAlgCategoryValues.ANY) { const symmetricSecretKey = params.symmetricSecretKey; @@ -998,8 +1023,9 @@ class _TokenEncoderService { } } - return ok({ - jwt: encodeJWTResult.value, + return ok({ + jwt: encodeJWTResult.value.jwt, + signingErrors: encodeJWTResult.value.signingErrors, }); } @@ -1235,6 +1261,7 @@ class _TokenEncoderService { } stateUpdate.jwt = processSymmetricSecretKeyResult.value.jwt.trim(); + stateUpdate.signingErrors = processSymmetricSecretKeyResult.value.signingErrors; return stateUpdate; } @@ -1269,6 +1296,7 @@ class _TokenEncoderService { } stateUpdate.jwt = processSymmetricSecretKeyResult.value.jwt.trim(); + stateUpdate.signingErrors = processSymmetricSecretKeyResult.value.signingErrors; return stateUpdate; } diff --git a/tests/token-encoder.service.test.ts b/tests/token-encoder.service.test.ts new file mode 100644 index 00000000..b76717c0 --- /dev/null +++ b/tests/token-encoder.service.test.ts @@ -0,0 +1,59 @@ +import { EncodingValues } from "@/features/common/values/encoding.values"; +import { describe, expect, test } from "vitest"; +import { TokenEncoderService } from "@/features/encoder/services/token-encoder.service"; +import { + DefaultTokensValues, + DefaultTokenWithSecretModel, +} from "@/features/common/values/default-tokens.values"; +import { EncoderResult } from "@/features/common/models/encoder-result.model"; + +describe("processSymmetricSecretKey", () => { + describe("should encode a JWT for SYMMETRIC type with HMAC algorithm", () => { + test("should return an object with a jwt and signingErrors should be null", async () => { + const params = { + header: JSON.stringify({ alg: "HS256", typ: "JWT" }), + payload: JSON.stringify({ + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }), + symmetricSecretKey: ( + DefaultTokensValues.HS256 as DefaultTokenWithSecretModel + ).secret, + symmetricSecretKeyEncoding: EncodingValues.UTF8, + }; + const result = + await TokenEncoderService.processSymmetricSecretKey(params); + + expect(result.isOk()).toBe(true); + expect(result.unwrapOr({})).toEqual({ + jwt: DefaultTokensValues.HS256.token, + signingErrors: null, + }); + }); + + test("should return an object with a jwt and signingErrors should not be null", async () => { + const params = { + header: JSON.stringify({ alg: "HS256", typ: "JWT" }), + payload: JSON.stringify({ + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }), + symmetricSecretKey: "secret", + symmetricSecretKeyEncoding: EncodingValues.UTF8, + }; + const algSize = 256; + const result = + await TokenEncoderService.processSymmetricSecretKey(params); + + expect(result.isOk()).toBe(true); + expect((result.unwrapOr({}) as EncoderResult).jwt).not.toBeNull(); + expect((result.unwrapOr({}) as EncoderResult).signingErrors).toEqual( + [`A key of ${algSize} bits or larger MUST be used with HS${algSize} as specified on [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518#section-3.2).`] + ); + }); + }); +});