diff --git a/README.md b/README.md index e99ff72..b902929 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ If you need to bundle this library manually yourself, be aware that this library - [Using the generic JWT RSA verifier for Cognito JWTs](#using-the-generic-jwt-rsa-verifier-for-cognito-jwts) - [Verifying JWTs from any OIDC-compatible IDP](#verifying-jwts-from-any-oidc-compatible-idp) - [Verify parameters](#jwtrsaverifier-verify-parameters) +- [Peeking inside unverified JWTs](#peeking-inside-unverified-jwts) - [Verification errors](#verification-errors) - [Peek inside invalid JWTs](#peek-inside-invalid-jwts) - [The JWKS cache](#the-jwks-cache) @@ -377,6 +378,21 @@ Supported parameters are: - `customJwtCheck` (optional): your custom function with additional JWT checks to execute (see [Custom JWT and JWK checks](#custom-jwt-and-jwk-checks)). - `includeRawJwtInErrors` (optional, default `false`): set to `true` if you want to peek inside the invalid JWT when verification fails. Refer to: [Peek inside invalid JWTs](#peek-inside-invalid-jwts). +## Peeking inside unverified JWTs + +You can peek into the payload of an unverified JWT as follows. + +Note: this does NOT verify a JWT, do not trust the returned payload and header! For most use cases, you would not want to call this function directly yourself, rather you would call `verify()` with the JWT, which would call this function (and others) for you. + +```typescript +import { decomposeUnverifiedJwt } from "aws-jwt-verify/jwt"; + +// danger! payload is sanity checked and JSON-parsed, but otherwise unverified, trust nothing in it! +const { payload } = decomposeUnverifiedJwt( + "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string +); +``` + ## Verification errors When verification of a JWT fails, this library will throw an error. All errors are defined in [src/error.ts](./src/error.ts) and can be imported and tested for like so: diff --git a/src/jwt-rsa.ts b/src/jwt-rsa.ts index 892b4c4..d3b2758 100644 --- a/src/jwt-rsa.ts +++ b/src/jwt-rsa.ts @@ -26,7 +26,11 @@ import { SupportedSignatureAlgorithm, } from "./jwt-model.js"; import { AsAsync, Properties } from "./typing-util.js"; -import { decomposeJwt, DecomposedJwt, validateJwtFields } from "./jwt.js"; +import { + decomposeUnverifiedJwt, + DecomposedJwt, + validateJwtFields, +} from "./jwt.js"; import { JwtInvalidClaimError, JwtInvalidIssuerError, @@ -234,11 +238,11 @@ export async function verifyJwt( includeRawJwtInErrors?: boolean; } ): Promise { - return verifyDecomposedJwt(decomposeJwt(jwt), jwksUri, options); + return verifyDecomposedJwt(decomposeUnverifiedJwt(jwt), jwksUri, options); } /** - * Verify (asynchronously) a JWT that is already decomposed (by function `decomposeJwt`) + * Verify (asynchronously) a JWT that is already decomposed (by function `decomposeUnverifiedJwt`) * * @param decomposedJwt The decomposed JWT * @param jwksUri The JWKS URI, where the JWKS can be fetched from @@ -335,7 +339,7 @@ export function verifyJwtSync( transformJwkToKeyObjectFn: JwkToKeyObjectTransformerSync = nodeWebCompat.transformJwkToKeyObjectSync ): JwtPayload { return verifyDecomposedJwtSync( - decomposeJwt(jwt), + decomposeUnverifiedJwt(jwt), jwkOrJwks, options, transformJwkToKeyObjectFn @@ -343,7 +347,7 @@ export function verifyJwtSync( } /** - * Verify (synchronously) a JWT that is already decomposed (by function `decomposeJwt`) + * Verify (synchronously) a JWT that is already decomposed (by function `decomposeUnverifiedJwt`) * * @param decomposedJwt The decomposed JWT * @param jwkOrJwks The JWKS that includes the right JWK (indexed by kid). Alternatively, provide the right JWK directly @@ -637,7 +641,7 @@ export abstract class JwtRsaVerifierBase< jwksUri: string; verifyProperties: SpecificVerifyProperties; } { - const decomposedJwt = decomposeJwt(jwt); + const decomposedJwt = decomposeUnverifiedJwt(jwt); assertStringArrayContainsString( "Issuer", decomposedJwt.payload.iss, diff --git a/src/jwt.ts b/src/jwt.ts index 51403b3..9fbfb07 100644 --- a/src/jwt.ts +++ b/src/jwt.ts @@ -83,21 +83,20 @@ function assertJwtPayload( } /** - * Sanity check, decompose and JSON parse a JWT string into its constituent parts: + * Sanity check, decompose and JSON parse a JWT string into its constituent, and yet unverified, parts: * - header object * - payload object * - signature string * + * This function does NOT verify a JWT, do not trust the returned payload and header! + * + * For most use cases, you would not want to call this function directly yourself, rather you + * would call verify() with the JWT, which would call this function (and others) for you. + * * @param jwt The JWT (as string) - * @returns the decomposed JWT + * @returns the decomposed, and yet unverified, JWT */ -export function decomposeJwt(jwt: unknown): { - header: JwtHeader; - headerB64: string; - payload: JwtPayload; - payloadB64: string; - signatureB64: string; -} { +export function decomposeUnverifiedJwt(jwt: unknown): DecomposedJwt { // Sanity checks on JWT if (!jwt) { throw new JwtParseError("Empty JWT"); @@ -150,7 +149,28 @@ export function decomposeJwt(jwt: unknown): { }; } -export type DecomposedJwt = ReturnType; +export interface DecomposedJwt { + /** + * The yet unverified (!) header of the JWT + */ + header: JwtHeader; + /** + * The yet unverified (!) header of the JWT, as base64url-encoded string + */ + headerB64: string; + /** + * The yet unverified (!) payload of the JWT + */ + payload: JwtPayload; + /** + * The yet unverified (!) payload of the JWT, as base64url-encoded string + */ + payloadB64: string; + /** + * The yet unverified (!) signature of the JWT, as base64url-encoded string + */ + signatureB64: string; +} /** * Validate JWT payload fields. Throws an error in case there's any validation issue. diff --git a/tests/unit/cognito-verifier.test.ts b/tests/unit/cognito-verifier.test.ts index f417996..a06378b 100644 --- a/tests/unit/cognito-verifier.test.ts +++ b/tests/unit/cognito-verifier.test.ts @@ -4,7 +4,7 @@ import { allowAllRealNetworkTraffic, disallowAllRealNetworkTraffic, } from "./test-util"; -import { decomposeJwt } from "../../src/jwt"; +import { decomposeUnverifiedJwt } from "../../src/jwt"; import { JwksCache, Jwks } from "../../src/jwk"; import { CognitoJwtVerifier } from "../../src/cognito-verifier"; import { @@ -38,7 +38,7 @@ describe("unit tests cognito verifier", () => { }, keypair.privateKey ); - const decomposedJwt = decomposeJwt(signedJwt); + const decomposedJwt = decomposeUnverifiedJwt(signedJwt); const customJwtCheck = jest.fn(); const cognitoVerifier = CognitoJwtVerifier.create({ userPoolId, diff --git a/tests/unit/jwt-rsa.test.ts b/tests/unit/jwt-rsa.test.ts index aaaadac..96eb4dc 100644 --- a/tests/unit/jwt-rsa.test.ts +++ b/tests/unit/jwt-rsa.test.ts @@ -7,7 +7,7 @@ import { publicKeyToJwk, base64url, } from "./test-util"; -import { decomposeJwt } from "../../src/jwt"; +import { decomposeUnverifiedJwt } from "../../src/jwt"; import { JwkInvalidUseError, JwkValidationError, @@ -1177,7 +1177,7 @@ describe("unit tests jwt verifier", () => { { aud: "1234", iss: issuer, hello: "world" }, keypair.privateKey ); - const decomposedJwt = decomposeJwt(signedJwt); + const decomposedJwt = decomposeUnverifiedJwt(signedJwt); verifier.verify(signedJwt, { audience: "1234" }).catch(() => { // This is intentional }); // The sync portion of this async function should call getJwk @@ -1308,7 +1308,7 @@ describe("unit tests jwt verifier", () => { { aud: audience, iss: issuer, hello: "world" }, keypair.privateKey ); - const decomposedJwt = decomposeJwt(signedJwt); + const decomposedJwt = decomposeUnverifiedJwt(signedJwt); const customJwtCheck = jest.fn(); const verifier = JwtRsaVerifier.create({ issuer, @@ -1388,7 +1388,7 @@ describe("unit tests jwt verifier", () => { { aud: audience, iss: issuer, hello: "world" }, keypair.privateKey ); - const decomposedJwt = decomposeJwt(signedJwt); + const decomposedJwt = decomposeUnverifiedJwt(signedJwt); const customJwtCheck = jest.fn().mockImplementation((jwt) => { if (jwt.header.typ !== "Expected JWT typ") { throw new Error("Oops my custom check failed");