import { IncomingMessage , ServerResponse } from 'http' ; import { generators } from 'openid-client' ; import { JWKS , JWS , JWK } from 'jose' ; import { signing as deriveKey } from './utils/hkdf' ; import { get as getCookie , clear as clearCookie , set as setCookie } from './utils/cookies' ; import { Config } from './config' ; export interface StoreOptions { sameSite ? : boolean | 'lax' | 'strict' | 'none' ; value ? : string ; } const header = { alg : 'HS256' , b64 : false , crit : [ 'b64' ] } ; const getPayload = ( cookie , value ) : Buffer => Buffer . from ( `${ cookie }=${ value }` ) ; const flattenedJWSFromCookie = ( cookie , value , signature ) : JWS . FlattenedJWS => ( { protected : Buffer . from ( JSON . stringify ( header ) ) . toString ( 'base64' ) . replace ( /=/g , '' ) . replace ( /\+/g , '-' ) . replace ( /\//g , '_' ) , payload : getPayload ( cookie , value ) , signature } ) ; const generateSignature = ( cookie , value , key ) : string => { const payload = getPayload ( cookie , value ) ; return JWS . sign . flattened ( payload , key , header ) . signature ; } ; const verifySignature = ( cookie , value , signature , keystore ) : boolean => { try { return ! ! JWS . verify ( flattenedJWSFromCookie ( cookie , value , signature ) , keystore , { algorithms : [ 'HS256' ] , crit : [ 'b64' ] } ) ; } catch ( err ) { return false ; } } ; const getCookieValue = ( cookie , value , keystore ) : string | undefined => { if ( ! value ) { return undefined ; } let signature ; [ value , signature ] = value . split ( '.' ) ; if ( verifySignature ( cookie , value , signature , keystore ) ) { return value ; } return undefined ; } ; export const generateCookieValue = ( cookie , value , key ) : string => { const signature = generateSignature ( cookie , value , key ) ; return `${ value }.${ signature }` ; } ; export default class TransientStore { private currentKey : JWK . Key | undefined ; private keyStore : JWKS . KeyStore ; constructor ( private config ) { let current ; const secret = config . secret ; const secrets = Array . isArray ( secret ) ? secret : [ secret ] ; const keystore = new JWKS . KeyStore ( ) ; secrets . forEach ( ( secretString , i ) => { const key = JWK . asKey ( deriveKey ( secretString ) ) ; if ( i === 0 ) { current = key ; } keystore . add ( key ) ; } ) ; this . currentKey = current ; this . keyStore = keystore ; } param { String } key param { IncomingMessage } _req param { ServerResponse } res param opts param { String } opts . sameSite param { String } opts . value return { String } save ( key , _req , res , { sameSite = 'none' , value = this . generateNonce ( ) } ) { const isSameSiteNone = sameSite === 'none' ; const { domain , path , secure } = this . config . session . cookie ; const basicAttr = { httpOnly : true , secure , domain , path } ; { const cookieValue = generateCookieValue ( key , value , this . currentKey as JWK . Key ) ; setCookie ( res , key , cookieValue , { ... basicAttr , sameSite , secure : isSameSiteNone ? true : basicAttr . secure } ) ; } if ( isSameSiteNone && this . config . legacySameSiteCookie ) { const cookieValue = generateCookieValue ( `_${ key }` , value , this . currentKey as JWK . Key ) ; setCookie ( res , `_${ key }` , cookieValue , basicAttr ) ; } return value ; } param { String } key param { IncomingMessage } req param { ServerResponse } res return { String | undefined } read ( key , req , res ) { const cookie = getCookie ( req , key ) ; const cookieConfig = this . config . session . cookie ; let value = getCookieValue ( key , cookie , this . keyStore ) ; clearCookie ( res , key , cookieConfig ) ; if ( this . config . legacySameSiteCookie ) { const fallbackKey = `_${ key }` ; if ( ! value ) { const fallbackCookie = getCookie ( req , fallbackKey ) ; value = getCookieValue ( fallbackKey , fallbackCookie , this . keyStore ) ; } clearCookie ( res , fallbackKey , cookieConfig ) ; } return value ; } return { String } generateNonce ( ) { return generators . nonce ( ) ; } return { String } generateCodeVerifier ( ) { return generators . codeVerifier ( ) ; } param { String } codeVerifier return { String } calculateCodeChallenge ( codeVerifier ) { return generators . codeChallenge ( codeVerifier ) ; } }