import { IncomingMessage } from 'http' ; import * as url from 'url' ; import { createLogger , ErrorWithCause } from '@sap-cloud-sdk/util' ; import { decode , verify } from 'jsonwebtoken' ; import { Jwt , JwtHeader , JwtPayload } from './jsonwebtoken-type' ; import { getXsuaaServiceCredentials } from './environment-accessor' ; import { TokenKey } from './xsuaa-service-types' ; import { XsuaaServiceCredentials } from './environment-accessor-types' ; import { Cache } from './cache' ; import { fetchVerificationKeys } from './verification-keys' ; const logger = createLogger ( { package : 'connectivity' , messageContext : 'jwt' } ) ; param token returns export function decodeJwt ( token ) { return decodeJwtComplete ( token ) . payload ; } param token returns internal export function decodeJwtComplete ( token ) { const decodedToken = decode ( token , { complete : true } ) ; if ( decodedToken === null || typeof decodedToken === 'string' ) { throw new Error ( 'JwtError: The given jwt payload does not encode valid JSON.' ) ; } return decodedToken ; } param req returns export function retrieveJwt ( req ) { const header = authHeader ( req ) ; if ( validateAuthHeader ( header ) ) { return header ! . split ( ' ' ) [ 1 ] ; } } function authHeader ( req ) { const entries = Object . entries ( req . headers ) . find ( ( [ key ] ) => key . toLowerCase ( ) === 'authorization' ) ; if ( entries ) { const header = entries [ 1 ] ; return Array . isArray ( header ) ? header [ 0 ] : header ; } return undefined ; } function validateAuthHeader ( header ) { if ( typeof header === 'undefined' ) { logger . warn ( 'Authorization header not set.' ) ; return false ; } const [ authType , token ] = header . split ( ' ' ) ; if ( typeof token === 'undefined' ) { logger . warn ( 'Token in auth header missing.' ) ; return false ; } if ( authType . toLowerCase ( ) !== 'bearer' ) { logger . warn ( 'Authorization type is not Bearer.' ) ; return false ; } return true ; } param header param uaaDomain internal function validateJwtHeaderForVerification ( header , uaaDomain ) { if ( ! header . jku || ! header . kid ) { throw new Error ( 'JWT does not contain verification key URL (`jku`) and/or key ID (`kid`).' ) ; } const jkuDomain = url . parse ( header . jku ) . hostname ; if ( ! uaaDomain || ! jkuDomain || ! jkuDomain . endsWith ( uaaDomain ) ) { throw new Error ( `The domains of the XSUAA and verification URL do not match. The XSUAA domain is '${ uaaDomain }' and the jku field provided in the JWT is '${ jkuDomain }'.` ) ; } } param token param options returns internal export async function verifyJwt ( token , options ? ) { options = { ... defaultVerifyJwtOptions , ... options } ; const creds = getXsuaaServiceCredentials ( token ) ; const header = decodeJwtComplete ( token ) . header ; validateJwtHeaderForVerification ( header , creds . uaadomain ) ; const cacheKey = buildCacheKey ( header . jku , header . kid ) ; if ( options . cacheVerificationKeys ) { const key = verificationKeyCache . get ( cacheKey ) ; if ( key ) { return verifyJwtWithKey ( token , key . value ) . catch ( error => { logger . warn ( 'Unable to verify JWT with cached key, fetching new verification key.' ) ; logger . warn ( `Original error: ${ error . message }` ) ; return fetchAndCacheKeyAndVerify ( creds , header , token , options ) ; } ) ; } } return fetchAndCacheKeyAndVerify ( creds , header , token , options ) ; } async function fetchAndCacheKeyAndVerify ( creds , header , token , options ? ) { const key = await getVerificationKey ( header ) . catch ( error => { throw new ErrorWithCause ( 'Failed to verify JWT. Could not retrieve verification key.' , error ) ; } ) ; if ( options ?. cacheVerificationKeys ) { verificationKeyCache . set ( buildCacheKey ( header . jku , header . kid ) , key ) ; } return verifyJwtWithKey ( token , key . value ) ; } /**
 * Options to control certain aspects of JWT verification behavior.
 */ export interface VerifyJwtOptions { cacheVerificationKeys ? : boolean ; } const defaultVerifyJwtOptions = { cacheVerificationKeys : true } ; function getVerificationKey ( header ) { if ( typeof header . jku !== 'string' ) { throw Error ( 'The JWT Header did not contain a XSUAA URL' ) ; } return fetchVerificationKeys ( header . jku ) . then ( verificationKeys => { if ( ! verificationKeys . length ) { throw Error ( 'No verification keys have been returned by the XSUAA service.' ) ; } const verificationKey = verificationKeys . find ( key => key . keyId === header . kid ) ; if ( ! verificationKey ) { throw new Error ( 'Could not find verification key for the given key ID.' ) ; } return verificationKey ; } ) ; } internal export const verificationKeyCache = new Cache < TokenKey > ( { minutes : 15 } ) ; function buildCacheKey ( jku , kid ) { if ( ! jku || ! kid ) { throw new Error ( 'Could not build cache key. `jku` and/or `kid` is not defined.' ) ; } return jku + kid ; } param token param key returns internal export function verifyJwtWithKey ( token , key ) { return new Promise ( ( resolve , reject ) => { verify ( token , sanitizeVerificationKey ( key ) , ( err , decodedToken ) => { if ( err ) { return reject ( new ErrorWithCause ( 'Invalid JWT.' , err ) ) ; } if ( ! decodedToken ) { return reject ( 'Invalid JWT. Token verification yielded `undefined`.' ) ; } return resolve ( decodedToken ) ; } ) ; } ) ; } function sanitizeVerificationKey ( key ) { return key . replace ( /\n/g , '' ) . replace ( /(KEY\s*-+)([^\n-])/ , '$1\n$2' ) . replace ( /([^\n-])(-+\s*END)/ , '$1\n$2' ) ; } param decodedToken returns internal export function issuerUrl ( decodedToken ) { return readPropertyWithWarn ( decodedToken , 'iss' ) ; } param decodedToken returns internal export function audiences ( decodedToken ) { if ( audiencesFromAud ( decodedToken ) . length ) { return new Set ( audiencesFromAud ( decodedToken ) ) ; } return new Set ( audiencesFromScope ( decodedToken ) ) ; } function audiencesFromAud ( decodedToken ) { if ( ! ( decodedToken . aud instanceof Array && decodedToken . aud . length ) ) { return [ ] ; } return decodedToken . aud . map ( aud => aud . includes ( '.' ) ? aud . substr ( 0 , aud . indexOf ( '.' ) ) : aud ) ; } function audiencesFromScope ( decodedToken ) { if ( ! decodedToken . scope ) { return [ ] ; } const scopes = decodedToken . scope instanceof Array ? decodedToken . scope : [ decodedToken . scope ] ; return scopes . reduce ( ( aud , scope ) => { if ( scope . includes ( '.' ) ) { return [ ... aud , scope . substr ( 0 , scope . indexOf ( '.' ) ) ] ; } return aud ; } , [ ] ) ; } param token returns internal export function wrapJwtInHeader ( token ) { return { headers : { Authorization : 'Bearer ' + token } } ; } internal param jwtPayload param property returns export function readPropertyWithWarn ( jwtPayload , property ) { if ( ! jwtPayload [ property ] ) { logger . warn ( `WarningJWT: The provided JWT payload does not include a '${ property }' property.` ) ; } return jwtPayload [ property ] ; } internal export type JwtKeyMapping < InterfaceT , JwtKeysT > = { [ key in keyof InterfaceT ] : { keyInJwt : JwtKeysT extends string ? JwtKeysT : keyof JwtKeysT ; extractorFunction : ( jwtPayload : JwtPayload ) => any ; } ; } ; param key param mapping param jwtPayload internal export function checkMandatoryValue < InterfaceT , JwtKeysT > ( key , mapping , jwtPayload ) { const value = mapping [ key ] . extractorFunction ( jwtPayload ) ; if ( ! value ) { throw new Error ( `Property '${ mapping [ key ] . keyInJwt }' is missing in JWT payload.` ) ; } } internal export interface JwtPair { decoded : JwtPayload ; encoded : string ; } param token returns internal export function isUserToken ( token ) { if ( ! token ) { return false ; } const keys = Object . keys ( token . decoded ) ; return ! ( keys . length === 1 && keys [ 0 ] === 'iss' ) ; }