Skip to content

Commit

Permalink
Merge pull request #5 from cwirving/jwt-kid
Browse files Browse the repository at this point in the history
Add support for additional header claims in signJWT() function
  • Loading branch information
Pinta365 authored Jul 22, 2024
2 parents 2e430eb + 209767f commit 51da4ac
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 7 deletions.
52 changes: 51 additions & 1 deletion mod.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { assertEquals, assertRejects } from "@std/assert";
import { test } from "@cross/test";
import { generateKey, generateKeyPair, signJWT, validateJWT } from "./mod.ts";
import { generateKey, generateKeyPair, signJWT, unsafeParseJOSEHeader, unsafeParseJWT, validateJWT } from "./mod.ts";
import { JWTAmbiguousClaimError, JWTFormatError, JWTValidationError } from "./src/error.ts";
import type { SupportedKeyAlgorithms, SupportedKeyPairAlgorithms } from "./src/cryptokeys.ts";
import type { JOSEHeader } from "./mod.ts";

test("signJWT() and validateJWT() with HMAC algorithms", async () => {
for (const algorithm of ["HS256", "HS384", "HS512"]) {
Expand Down Expand Up @@ -128,3 +129,52 @@ test("signJWT() works with 'notBefore' only", async () => {
const decoded = await validateJWT(jwt, secret);
assertEquals(typeof decoded.nbf, "number");
});

test("signJWT() supports additional header claims", async () => {
const algorithm: SupportedKeyPairAlgorithms = "RS256";
const { privateKey, publicKey } = await generateKeyPair(algorithm);
const payload = { foo: "bar", baz: 42 };
const jwtString = await signJWT(payload, privateKey, {
algorithm: algorithm,
additionalHeaderClaims: { typ: "JOSE", kid: "abc123" },
});

const unsafeHeader = unsafeParseJOSEHeader(jwtString);
const unsafePayload = unsafeParseJWT(jwtString);
const decodedPayload = await validateJWT(jwtString, publicKey, { algorithm });

assertEquals(unsafePayload, payload);
assertEquals(decodedPayload, payload);
const expectedHeader: JOSEHeader = { alg: algorithm, typ: "JOSE", kid: "abc123" };
assertEquals(unsafeHeader, expectedHeader);
});

test("validateJWT() ignores the `alg` header claim", async () => {
const algorithm: SupportedKeyPairAlgorithms = "RS256";
const { privateKey, publicKey } = await generateKeyPair(algorithm);
const payload = { foo: "bar", baz: 42 };
const jwtString = await signJWT(payload, privateKey, {
algorithm: algorithm,
additionalHeaderClaims: { alg: "none" }, // Note the algorithm mismatch
});

// Should use the algorithm present in the public key, not the algorithm claimed by the JWT.
const decodedPayload = await validateJWT(jwtString, publicKey);
assertEquals(decodedPayload, payload);
});

test("validateJWT() throws JWTValidationError on stripped token", async () => {
const algorithm: SupportedKeyPairAlgorithms = "RS256";
const keyPair = await generateKeyPair(algorithm);

const payload = { foo: "bar", baz: 42 };

// Simulates the scenario where the attacker tampered with the body and tries to pass an
// unsigned token as the real item.
const jwtString = await signJWT(payload, false);

assertRejects(
() => validateJWT(jwtString, keyPair.publicKey),
JWTValidationError,
);
});
4 changes: 2 additions & 2 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// mod.ts
export { signJWT } from "./src/sign.ts";
export { unsafeParseJWT, validateJWT } from "./src/validate.ts";
export { unsafeParseJOSEHeader, unsafeParseJWT, validateJWT } from "./src/validate.ts";
export { exportPEMKey, generateKey, generateKeyPair, importPEMKey } from "./src/cryptokeys.ts";
export type {
ExportPEMKeyOptions,
Expand All @@ -10,7 +10,7 @@ export type {
SupportedKeyPairAlgorithms,
} from "./src/cryptokeys.ts";
export type { JWTOptions } from "./src/options.ts";
export type { JWTPayload } from "./src/standardclaims.ts";
export type { JOSEHeader, JWTPayload } from "./src/standardclaims.ts";

//Aliases
export { signJWT as createJWT } from "./src/sign.ts";
Expand Down
7 changes: 7 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { JOSEHeader } from "./standardclaims.ts";

/**
* Options for customizing JWT creation and parsing behavior.
*/
Expand Down Expand Up @@ -40,6 +42,11 @@ export interface JWTOptions {
* Cannot be used if the `nbf` claim is explicitly set in the payload.
*/
notBefore?: string;

/**
* Additional claims to include as part of the JWT's JOSE header.
*/
additionalHeaderClaims?: JOSEHeader;
}

/**
Expand Down
8 changes: 6 additions & 2 deletions src/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { signWithHMAC } from "./sign-verify/hmac.ts";
import { signWithECDSA } from "./sign-verify/ecdsa.ts";
import { signWithRSAPSS } from "./sign-verify/rsapss.ts";

import type { JWTPayload } from "./standardclaims.ts";
import type { JOSEHeader, JWTPayload } from "./standardclaims.ts";

/**
* Parses a duration string like "1d", "2h", "30m", or "15s" and returns the equivalent time in seconds.
Expand Down Expand Up @@ -120,7 +120,11 @@ export async function signJWT(

validateClaims(payload, options);

const header = { alg: algorithm, typ: "JWT" };
const header: JOSEHeader = { alg: algorithm, typ: "JWT" };
if (options?.additionalHeaderClaims !== undefined) {
Object.assign(header, options.additionalHeaderClaims);
}

const encodedHeader = encodeBase64Url(textEncode(JSON.stringify(header)));
const encodedPayload = encodeBase64Url(textEncode(JSON.stringify(payload)));

Expand Down
34 changes: 34 additions & 0 deletions src/standardclaims.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
/**
* The the JOSE header part of a JWT, JWS or JWE structure.
*
* Note: only some of the more common claims are defined here. See RFC 7519 (https://tools.ietf.org/html/rfc7519),
* RFC 7515 (https://tools.ietf.org/html/rfc7515) and RFC 7516 (https://tools.ietf.org/html/rfc7516) for a
* full list of standard header claims and their explanations.
*/
export interface JOSEHeader {
/**
* The cryptographic algorithm used to secure the JWS/JWE structure.
* If unspecified, the `signJWT()` function will use the algorithm of the provided key
* as the value of this header claim.
*
* (See RFC 7515 section 4.1.1, RFC 7516 section 4.1.1)
*/
alg?: string;

/**
* When the token is signed with a key from a JSON Web Key set (JWKS), this is the identifier
* of the key in the JWKS.
*
* (See RFC 7515 section 4.1.4, RFC 7516 section 4.1.6)
*/
kid?: string;

/**
* The media type of the complete JWT/JWS/JWE. If unspecified, the `signJWT()`
* function will use "JWT" as the value of this header claim.
*
* (see RFC 7519 section 5.1, RFC 7515 section 4.1.9, RFC 7516 section 4.1.11)
*/
typ?: string;
}

/**
* Represents the payload of a JWT. Includes optional standard claims and allows for the
* addition of custom properties.
Expand Down
21 changes: 19 additions & 2 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { verifyWithHMAC } from "./sign-verify/hmac.ts";
import { verifyWithECDSA } from "./sign-verify/ecdsa.ts";
import { verifyWithRSAPSS } from "./sign-verify/rsapss.ts";

import type { JWTPayload } from "./standardclaims.ts";
import type { JOSEHeader, JWTPayload } from "./standardclaims.ts";

/**
* Validates and parses a JWT, verifies it with the given key, and returns the contained payload.
Expand Down Expand Up @@ -243,7 +243,7 @@ export async function verify(key: CryptoKey, data: string, signature: string, op
* "unsafely" parse a JWT without cryptokey.
*
* @param {string} jwt - The encoded JWT string.
* @returns {JWTPayload} A promise resolving to the decoded JWT payload.
* @returns {JWTPayload} The decoded JWT payload.
* @throws {JWTParseError} If the jwt string is not parsable.
*/
export function unsafeParseJWT(jwt: string): JWTPayload {
Expand All @@ -255,3 +255,20 @@ export function unsafeParseJWT(jwt: string): JWTPayload {
throw new JWTParseError(error);
}
}

/**
* "unsafely" parse the JOSE header of a JWT without cryptokey.
*
* @param {string} jwt - The encoded JWT string.
* @returns {JOSEHeader} The decoded header portion of the JWT.
* @throws {JWTParseError} If the jwt string is not parsable.
*/
export function unsafeParseJOSEHeader(jwt: string): JOSEHeader {
try {
const jwtParts = validateParts(jwt);
const payload = JSON.parse(textDecode(decodeBase64Url(jwtParts[0])));
return payload;
} catch (error) {
throw new JWTParseError(error);
}
}

0 comments on commit 51da4ac

Please sign in to comment.