Skip to content

Remove secret length requirement #869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/features/common/models/encoder-result.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface EncoderResult {
jwt: string;
signingErrors: string[] | null;
}
128 changes: 78 additions & 50 deletions src/features/encoder/services/token-encoder.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -379,7 +381,7 @@ class _TokenEncoderService {
}

if (encodeJWTResult.isOk()) {
stateUpdate.jwt = encodeJWTResult.value.trim();
stateUpdate.jwt = encodeJWTResult.value.jwt.trim();
}

return {
Expand Down Expand Up @@ -409,7 +411,7 @@ class _TokenEncoderService {
}

if (encodeJWTResult.isOk()) {
stateUpdate.jwt = encodeJWTResult.value.trim();
stateUpdate.jwt = encodeJWTResult.value.jwt.trim();
}

return {
Expand Down Expand Up @@ -484,48 +486,61 @@ class _TokenEncoderService {
payload: DecodedJwtPayloadModel,
key: string,
encodingFormat: EncodingValues,
): Promise<Result<string, DebuggerErrorModel>> {
if (isHmacAlg(header.alg)) {
if (!key) {
return err({
task: DebuggerTaskValues.ENCODE,
input: DebuggerInputValues.KEY,
message: "Secret must not be empty.",
});
}
): Promise<Result<EncoderResult, DebuggerErrorModel>> {
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<EncoderResult>({
jwt: signWithSymmetricSecretKeyResult.value,
signingErrors: signingError,
});
}

Expand All @@ -534,7 +549,7 @@ class _TokenEncoderService {
payload: DecodedJwtPayloadModel,
key: string,
keyFormat: AsymmetricKeyFormatValues,
): Promise<Result<string, DebuggerErrorModel>> {
): Promise<Result<EncoderResult, DebuggerErrorModel>> {
if (isDigitalSignatureAlg(header.alg)) {
if (!key) {
return err({
Expand All @@ -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({
Expand Down Expand Up @@ -684,9 +712,7 @@ class _TokenEncoderService {
symmetricSecretKeyEncoding: EncodingValues;
}): Promise<
Result<
{
jwt: string;
},
EncoderResult,
EncodingSymmetricSecretKeyErrors
>
> {
Expand Down Expand Up @@ -767,6 +793,7 @@ class _TokenEncoderService {

return ok({
jwt: encodeJwtResult.value.jwt.trim(),
signingErrors: encodeJwtResult.value.signingErrors,
});
}

Expand Down Expand Up @@ -861,17 +888,15 @@ class _TokenEncoderService {
},
): Promise<
Result<
{
jwt: string;
},
EncoderResult,
EncodingJwtErrors
>
> {
const algType = params.algType;
const header = params.header;
const payload = params.payload;

let encodeJWTResult: Result<string, DebuggerErrorModel> | null = null;
let encodeJWTResult: Result<EncoderResult, DebuggerErrorModel> | null = null;

if (algType === SigningAlgCategoryValues.ANY) {
const symmetricSecretKey = params.symmetricSecretKey;
Expand Down Expand Up @@ -998,8 +1023,9 @@ class _TokenEncoderService {
}
}

return ok({
jwt: encodeJWTResult.value,
return ok<EncoderResult>({
jwt: encodeJWTResult.value.jwt,
signingErrors: encodeJWTResult.value.signingErrors,
});
}

Expand Down Expand Up @@ -1235,6 +1261,7 @@ class _TokenEncoderService {
}

stateUpdate.jwt = processSymmetricSecretKeyResult.value.jwt.trim();
stateUpdate.signingErrors = processSymmetricSecretKeyResult.value.signingErrors;

return stateUpdate;
}
Expand Down Expand Up @@ -1269,6 +1296,7 @@ class _TokenEncoderService {
}

stateUpdate.jwt = processSymmetricSecretKeyResult.value.jwt.trim();
stateUpdate.signingErrors = processSymmetricSecretKeyResult.value.signingErrors;

return stateUpdate;
}
Expand Down
59 changes: 59 additions & 0 deletions tests/token-encoder.service.test.ts
Original file line number Diff line number Diff line change
@@ -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).`]
);
});
});
});