import { createVerify , createPublicKey , KeyObject } from "crypto" ; import { URL } from "url" ; import { join } from "path" ; import { SimpleJwksCache , JwksCache , Jwk , Jwks , isJwk , isJwks , fetchJwk , } from "./jwk.js" ; import { constructPublicKeyInDerFormat } from "./asn1.js" ; import { assertIsNotPromise , assertStringArrayContainsString , assertStringEquals , } from "./assert.js" ; import { JwtHeader , JwtPayload } from "./jwt-model.js" ; import { Properties } from "./typing-util.js" ; import { decomposeJwt , DecomposedJwt , validateJwtFields } from "./jwt.js" ; import { JwkInvalidKtyError , JwkInvalidUseError , JwtInvalidClaimError , JwtInvalidIssuerError , JwtInvalidSignatureAlgorithmError , JwtInvalidSignatureError , KidNotFoundInJwksError , ParameterValidationError , } from "./error.js" ; import { JsonObject } from "./safe-json-parse.js" ; /** Interface for JWT verification properties */ export interface VerifyProperties { /**
     * The audience that you expect to be present in the JWT's aud claim.
     * If you provide a string array, that means at least one of those audiences
     * must be present in the JWT's aud claim.
     * Pass null explicitly to not check the JWT's audience--if you know what you're doing
     */ audience : string | string [ ] | null ; /**
     * The scope that you expect to be present in the JWT's scope claim.
     * If you provide a string array, that means at least one of those scopes
     * must be present in the JWT's scope claim.
     */ scope ? : string | string [ ] ; /**
     * The number of seconds after expiration (exp claim) or before not-before (nbf claim) that you will allow
     * (use this to account for clock differences between systems)
     */ graceSeconds ? : number ; /**
     * Your custom function with checks. It will be called, at the end of the verification,
     * after standard verifcation checks have all passed.
     * Throw an error in this function if you want to reject the JWT for whatever reason you deem fit.
     * Your function will be called with a properties object that contains:
     * - the decoded JWT header
     * - the decoded JWT payload
     * - the JWK that was used to verify the JWT's signature
     */ customJwtCheck ? : ( props : { header : JwtHeader ; payload : JwtPayload ; jwk : Jwk ; } ) => Promise < void > | void ; /**
     * If you want to peek inside the invalid JWT when verification fails, set `includeRawJwtInErrors` to true.
     * Then, if an error is thrown during verification of the invalid JWT (e.g. the JWT is invalid because it is expired),
     * the Error object will include a property `rawJwt`, with the raw decoded contents of the **invalid** JWT.
     * The `rawJwt` will only be included in the Error object, if the JWT's signature can at least be verified.
     */ includeRawJwtInErrors ? : boolean ; } /** Type for JWT RSA verifier properties, for a single issuer */ export type JwtRsaVerifierProperties < VerifyProps > = { /**
     * URI where the JWKS (JSON Web Key Set) can be downloaded from.
     * The JWKS contains one or more JWKs, which represent the public keys with which
     * JWTs have been signed.
     */ jwksUri ? : string ; /**
     * The issuer of the JWTs you want to verify.
     * Set this to the expected value of the `iss` claim in the JWT.
     */ issuer : string ; } & Partial < VerifyProps > ; /**
 * Type for JWT RSA verifier properties, when multiple issuers are used in the verifier.
 * In this case, you should be explicit in mapping audience to issuer.
 */ export type JwtRsaVerifierMultiProperties < T > = { /**
     * URI where the JWKS (JSON Web Key Set) can be downloaded from.
     * The JWKS contains one or more JWKs, which represent the public keys with which
     * JWTs have been signed.
     */ jwksUri ? : string ; /**
     * The issuer of the JWTs you want to verify.
     * Set this to the expected value of the `iss` claim in the JWT.
     */ issuer : string ; } & T ; /**
 * JWT Verifier (RSA) for a single issuer
 */ export type JwtRsaVerifierSingleIssuer < T extends JwtRsaVerifierProperties < VerifyProperties > > = JwtRsaVerifier < Properties < VerifyProperties , T > , T & JwtRsaVerifierProperties < VerifyProperties > , false > ; /**
 * Parameters used for verification of a JWT.
 * The first parameter is the JWT, which is (of course) mandatory.
 * The second parameter is an object with specific properties to use during verification.
 * The second parameter is only mandatory if its mandatory members (e.g. audience) were not
 *  yet provided at verifier level. In that case, they must now be provided.
 */ type VerifyParameters < SpecificVerifyProperties > = { [ key ] : never ; } extends SpecificVerifyProperties ? [ jwt : string , props ? : SpecificVerifyProperties ] : [ jwt : string , props : SpecificVerifyProperties ] ; /**
 * JWT Verifier (RSA) for multiple issuers
 */ export type JwtRsaVerifierMultiIssuer < T extends JwtRsaVerifierMultiProperties < VerifyProperties > > = JwtRsaVerifier < Properties < VerifyProperties , T > , T & JwtRsaVerifierProperties < VerifyProperties > , true > ; /**
 * Enum to map supported JWT signature algorithms with OpenSSL message digest algorithm names
 */ export enum JwtSignatureAlgorithms { RS256 = "RSA-SHA256" , RS384 = "RSA-SHA384" , RS512 = "RSA-SHA512" } param header param headerB64 param payload param payloadB64 param signatureB64 param jwk param jwkToKeyObjectTransformer returns function verifySignatureAgainstJwk ( header , headerB64 , payload , payloadB64 , signatureB64 , jwk , jwkToKeyObjectTransformer = transformJwkToKeyObject ) { assertStringEquals ( "JWK use" , jwk . use , "sig" , JwkInvalidUseError ) ; assertStringEquals ( "JWK kty" , jwk . kty , "RSA" , JwkInvalidKtyError ) ; if ( jwk . alg ) { assertStringEquals ( "JWT signature algorithm" , header . alg , jwk . alg , JwtInvalidSignatureAlgorithmError ) ; } assertStringArrayContainsString ( "JWT signature algorithm" , header . alg , [ "RS256" , "RS384" , "RS512" ] , JwtInvalidSignatureAlgorithmError ) ; const publicKey = jwkToKeyObjectTransformer ( jwk , payload . iss , header . kid ) ; const valid = createVerify ( JwtSignatureAlgorithms [ header . alg as keyof typeof JwtSignatureAlgorithms ] ) . update ( `${ headerB64 }.${ payloadB64 }` ) . verify ( publicKey , signatureB64 , "base64" ) ; if ( ! valid ) { throw new JwtInvalidSignatureError ( "Invalid signature" ) ; } } param jwt param jwksUri param options param jwkFetcher param jwkToKeyObjectTransformer returns export async function verifyJwt ( jwt , jwksUri , options , jwkFetcher ? , jwkToKeyObjectTransformer ? ) { return verifyDecomposedJwt ( decomposeJwt ( jwt ) , jwksUri , options , jwkFetcher , jwkToKeyObjectTransformer ) ; } param decomposedJwt param jwksUri param options param jwkFetcher param jwkToKeyObjectTransformer returns async function verifyDecomposedJwt ( decomposedJwt , jwksUri , options , jwkFetcher = fetchJwk , jwkToKeyObjectTransformer ? ) { const { header , headerB64 , payload , payloadB64 , signatureB64 } = decomposedJwt ; const jwk = await jwkFetcher ( jwksUri , decomposedJwt ) ; verifySignatureAgainstJwk ( header , headerB64 , payload , payloadB64 , signatureB64 , jwk , jwkToKeyObjectTransformer ) ; try { validateJwtFields ( payload , options ) ; if ( options . customJwtCheck ) { await options . customJwtCheck ( { header , payload , jwk } ) ; } } catch ( err ) { if ( options . includeRawJwtInErrors && err instanceof JwtInvalidClaimError ) { throw err . withRawJwt ( decomposedJwt ) ; } throw err ; } return payload ; } param jwt param jwkOrJwks param options param jwkToKeyObjectTransformer returns export function verifyJwtSync ( jwt , jwkOrJwks , options , jwkToKeyObjectTransformer ? ) { return verifyDecomposedJwtSync ( decomposeJwt ( jwt ) , jwkOrJwks , options , jwkToKeyObjectTransformer ) ; } param decomposedJwt param jwkOrJwks param options param jwkToKeyObjectTransformer returns function verifyDecomposedJwtSync ( decomposedJwt , jwkOrJwks , options , jwkToKeyObjectTransformer ? ) { const { header , headerB64 , payload , payloadB64 , signatureB64 } = decomposedJwt ; let jwk ; if ( isJwk ( jwkOrJwks ) ) { jwk = jwkOrJwks ; } else if ( isJwks ( jwkOrJwks ) ) { const locatedJwk = jwkOrJwks . keys . find ( ( key ) => key . kid === header . kid ) ; if ( ! locatedJwk ) { throw new KidNotFoundInJwksError ( `JWK for kid ${ header . kid } not found in the JWKS` ) ; } jwk = locatedJwk ; } else { throw new ParameterValidationError ( [ `Expected a valid JWK or JWKS (parsed as JavaScript object), but received: ${ jwkOrJwks }.` , "If you're passing a JWKS URI, use the async verify() method instead, it will download and parse the JWKS for you" , ] . join ( ) ) ; } verifySignatureAgainstJwk ( header , headerB64 , payload , payloadB64 , signatureB64 , jwk , jwkToKeyObjectTransformer ) ; try { validateJwtFields ( payload , options ) ; if ( options . customJwtCheck ) { const res = options . customJwtCheck ( { header , payload , jwk } ) ; assertIsNotPromise ( res , ( ) => new ParameterValidationError ( "Custom JWT checks must be synchronous but a promise was returned" ) ) ; } } catch ( err ) { if ( options . includeRawJwtInErrors && err instanceof JwtInvalidClaimError ) { throw err . withRawJwt ( decomposedJwt ) ; } throw err ; } return payload ; } /** Type alias for better readability below */ type Issuer = string ; type Kid = string ; param StillToProvide param SpecificVerifyProperties param IssuerConfig param MultiIssuer export abstract class JwtRsaVerifierBase < SpecificVerifyProperties , IssuerConfig extends JwtRsaVerifierProperties < SpecificVerifyProperties > , MultiIssuer extends boolean > { private issuersConfig : Map < Issuer , IssuerConfig & { jwksUri : string ; } > = new Map ( ) ; private publicKeyCache = new KeyObjectCache ( ) ; protected constructor ( verifyProperties , private jwksCache = new SimpleJwksCache ( ) ) { if ( Array . isArray ( verifyProperties ) ) { if ( ! verifyProperties . length ) { throw new ParameterValidationError ( "Provide at least one issuer configuration" ) ; } for ( const prop of verifyProperties ) { if ( this . issuersConfig . has ( prop . issuer ) ) { throw new ParameterValidationError ( `issuer ${ prop . issuer } supplied multiple times` ) ; } this . issuersConfig . set ( prop . issuer , this . withJwksUri ( prop ) ) ; } } else { this . issuersConfig . set ( verifyProperties . issuer , this . withJwksUri ( verifyProperties ) ) ; } } protected get expectedIssuers ( ) : string [ ] { return Array . from ( this . issuersConfig . keys ( ) ) ; } protected getIssuerConfig ( issuer ? ) { if ( ! issuer ) { if ( this . issuersConfig . size !== 1 ) { throw new ParameterValidationError ( "issuer must be provided" ) ; } issuer = this . issuersConfig . keys ( ) . next ( ) . value ; } const config = this . issuersConfig . get ( issuer ! ) ; if ( ! config ) { throw new ParameterValidationError ( `issuer not configured: ${ issuer }` ) ; } return config ; } param jwksThe param issuer returns public cacheJwks ( ... [ jwks , issuer ] ) { const issuerConfig = this . getIssuerConfig ( issuer ) ; this . jwksCache . addJwks ( issuerConfig . jwksUri , jwks ) ; this . publicKeyCache . clearCache ( issuerConfig . issuer ) ; } param issuer returns async hydrate ( ) { const jwksFetches = this . expectedIssuers . map ( ( issuer ) => this . getIssuerConfig ( issuer ) . jwksUri ) . map ( ( jwksUri ) => this . jwksCache . getJwks ( jwksUri ) ) ; await Promise . all ( jwksFetches ) ; } param jwt param props returns public verifySync ( ... [ jwt , properties ] ) { const { decomposedJwt , jwksUri , verifyProperties } = this . getVerifyParameters ( jwt , properties ) ; return this . verifyDecomposedJwtSync ( decomposedJwt , jwksUri , verifyProperties ) ; } param decomposedJwt param jwk param verifyProperties returns protected verifyDecomposedJwtSync ( decomposedJwt , jwksUri , verifyProperties ) { const jwk = this . jwksCache . getCachedJwk ( jwksUri , decomposedJwt ) ; return verifyDecomposedJwtSync ( decomposedJwt , jwk , verifyProperties , this . publicKeyCache . transformJwkToKeyObject . bind ( this . publicKeyCache ) ) ; } param jwt param props returns public async verify ( ... [ jwt , properties ] ) { const { decomposedJwt , jwksUri , verifyProperties } = this . getVerifyParameters ( jwt , properties ) ; return this . verifyDecomposedJwt ( decomposedJwt , jwksUri , verifyProperties ) ; } param decomposedJwt param jwk param verifyProperties returns protected verifyDecomposedJwt ( decomposedJwt , jwksUri , verifyProperties ) { return verifyDecomposedJwt ( decomposedJwt , jwksUri , verifyProperties , this . jwksCache . getJwk . bind ( this . jwksCache ) , this . publicKeyCache . transformJwkToKeyObject . bind ( this . publicKeyCache ) ) ; } param jwt param verifyProperties returns protected getVerifyParameters ( jwt , verifyProperties ? ) { const decomposedJwt = decomposeJwt ( jwt ) ; assertStringArrayContainsString ( "Issuer" , decomposedJwt . payload . iss , this . expectedIssuers , JwtInvalidIssuerError ) ; const issuerConfig = this . getIssuerConfig ( decomposedJwt . payload . iss ) ; return { decomposedJwt , jwksUri : issuerConfig . jwksUri , verifyProperties : { ... issuerConfig , ... verifyProperties , } as unknown as SpecificVerifyProperties , } ; } param config returns private withJwksUri ( config ) { if ( config . jwksUri ) { return config as IssuerConfig & { jwksUri : string ; } ; } const issuerUri = new URL ( config . issuer ) ; return { jwksUri : new URL ( join ( issuerUri . pathname , "/.well-known/jwks.json" ) , config . issuer ) . href , ... config , } ; } } /**
 * Class representing a verifier for JWTs signed with RSA (e.g. RS256 / RS384 / RS512)
 */ export class JwtRsaVerifier < SpecificVerifyProperties extends Partial < VerifyProperties > , IssuerConfig extends JwtRsaVerifierProperties < SpecificVerifyProperties > , MultiIssuer extends boolean > extends JwtRsaVerifierBase < SpecificVerifyProperties , IssuerConfig , MultiIssuer > { param verifyProperties param additionalProperties param additionalProperties . jwksCache returns static create < T extends JwtRsaVerifierProperties < VerifyProperties > > ( verifyProperties , additionalProperties ? ) ; param verifyProperties param additionalProperties param additionalProperties . jwksCache returns static create < T extends JwtRsaVerifierMultiProperties < VerifyProperties > > ( verifyProperties , additionalProperties ? ) ; static create ( verifyProperties , additionalProperties ? ) { return new this ( verifyProperties , additionalProperties ?. jwksCache ) ; } } /** Interface for functions that can transform a JWK into an RSA public key in Node.js native key object format */ export type JwkToKeyObjectTransformer = ( jwk : Jwk , issuer ? : string , kid ? : string ) => KeyObject ; param jwk returns export const transformJwkToKeyObject = ( jwk ) => createPublicKey ( { key : constructPublicKeyInDerFormat ( Buffer . from ( jwk . n , "base64" ) , Buffer . from ( jwk . e , "base64" ) ) , format : "der" , type : "spki" , } ) ; /**
 * Class representing a cache of RSA public keys in Node.js native key object format
 *
 * Because it takes a bit of compute time to turn a JWK into Node.js native key object format,
 * we want to cache this computation.
 */ export class KeyObjectCache { private publicKeys : Map < Issuer , Map < Kid , KeyObject > > = new Map ( ) ; constructor ( public jwkToKeyObjectTransformer = transformJwkToKeyObject ) { } param jwk param issuer returns transformJwkToKeyObject ( jwk , issuer ? ) { if ( ! issuer ) { return this . jwkToKeyObjectTransformer ( jwk ) ; } const cachedPublicKey = this . publicKeys . get ( issuer ) ?. get ( jwk . kid ) ; if ( cachedPublicKey ) { return cachedPublicKey ; } const publicKey = this . jwkToKeyObjectTransformer ( jwk ) ; const cachedIssuer = this . publicKeys . get ( issuer ) ; if ( cachedIssuer ) { cachedIssuer . set ( jwk . kid , publicKey ) ; } else { this . publicKeys . set ( issuer , new Map ( [ [ jwk . kid , publicKey ] ] ) ) ; } return publicKey ; } clearCache ( issuer ) { this . publicKeys . delete ( issuer ) ; } }