import { inflateString , base64Decode , isNonEmptyArray } from './utility' ; import { verifyTime } from './validator' ; import libsaml from './libsaml' ; import { extract , loginRequestFields , loginResponseFields , logoutRequestFields , logoutResponseFields , ExtractorFields , logoutResponseStatusFields , loginResponseStatusFields } from './extractor' ; import { BindingNamespace , ParserType , wording , MessageSignatureOrder , StatusCode } from './urn' ; import simpleSignBinding from './binding-simplesign' ; const bindDict = wording . binding ; const urlParams = wording . urlParams ; export interface FlowResult { samlContent : string ; extract : any ; sigAlg ? : string | null ; } function getDefaultExtractorFields ( parserType , assertion ? ) { switch ( parserType ) { case ParserType . SAMLRequest : return loginRequestFields ; case ParserType . SAMLResponse : if ( ! assertion ) { throw new Error ( 'ERR_EMPTY_ASSERTION' ) ; } return loginResponseFields ( assertion ) ; case ParserType . LogoutRequest : return logoutRequestFields ; case ParserType . LogoutResponse : return logoutResponseFields ; default : throw new Error ( 'ERR_UNDEFINED_PARSERTYPE' ) ; } } async function redirectFlow ( options ) { const { request , parserType , self , checkSignature = true , from } = options ; const { query , octetString } = request ; const { SigAlg : sigAlg , Signature : signature } = query ; const targetEntityMetadata = from . entityMeta ; const direction = libsaml . getQueryParamByType ( parserType ) ; const content = query [ direction ] ; if ( content === undefined ) { return Promise . reject ( 'ERR_REDIRECT_FLOW_BAD_ARGS' ) ; } const xmlString = inflateString ( decodeURIComponent ( content ) ) ; try { await libsaml . isValidXml ( xmlString ) ; } catch ( e ) { return Promise . reject ( 'ERR_INVALID_XML' ) ; } await checkStatus ( xmlString , parserType ) ; let assertion = '' ; if ( parserType === urlParams . samlResponse ) { const verifiedDoc = extract ( xmlString , [ { key : 'assertion' , localPath : [ '~Response' , 'Assertion' ] , attributes : [ ] , context : true } ] ) ; if ( verifiedDoc && verifiedDoc . assertion ) { assertion = verifiedDoc . assertion as string ; } } const extractorFields = getDefaultExtractorFields ( parserType , assertion . length > 0 ? assertion : null ) ; const parseResult = { samlContent : xmlString , sigAlg : null , extract : extract ( xmlString , extractorFields ) , } ; if ( checkSignature ) { if ( ! signature || ! sigAlg ) { return Promise . reject ( 'ERR_MISSING_SIG_ALG' ) ; } const base64Signature = Buffer . from ( decodeURIComponent ( signature ) , 'base64' ) ; const decodeSigAlg = decodeURIComponent ( sigAlg ) ; const verified = libsaml . verifyMessageSignature ( targetEntityMetadata , octetString , base64Signature , sigAlg ) ; if ( ! verified ) { return Promise . reject ( 'ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION' ) ; } parseResult . sigAlg = decodeSigAlg ; } /**
     *  Validation part: validate the context of response after signature is verified and decrpyted (optional)
     */ const issuer = targetEntityMetadata . getEntityID ( ) ; const extractedProperties = parseResult . extract ; if ( ( parserType === 'LogoutResponse' || parserType === 'SAMLResponse' ) && extractedProperties && extractedProperties . issuer !== issuer ) { return Promise . reject ( 'ERR_UNMATCH_ISSUER' ) ; } if ( parserType === 'SAMLResponse' && extractedProperties . sessionIndex . sessionNotOnOrAfter && ! verifyTime ( undefined , extractedProperties . sessionIndex . sessionNotOnOrAfter , self . entitySetting . clockDrifts ) ) { return Promise . reject ( 'ERR_EXPIRED_SESSION' ) ; } if ( parserType === 'SAMLResponse' && extractedProperties . conditions && ! verifyTime ( extractedProperties . conditions . notBefore , extractedProperties . conditions . notOnOrAfter , self . entitySetting . clockDrifts ) ) { return Promise . reject ( 'ERR_SUBJECT_UNCONFIRMED' ) ; } return Promise . resolve ( parseResult ) ; } async function postFlow ( options ) { const { request , from , self , parserType , checkSignature = true } = options ; const { body } = request ; const direction = libsaml . getQueryParamByType ( parserType ) ; const encodedRequest = body [ direction ] ; let samlContent = String ( base64Decode ( encodedRequest ) ) ; const verificationOptions = { metadata : from . entityMeta , signatureAlgorithm : from . entitySetting . requestSignatureAlgorithm , } ; const decryptRequired = from . entitySetting . isAssertionEncrypted ; let extractorFields = [ ] ; await libsaml . isValidXml ( samlContent ) ; if ( parserType !== urlParams . samlResponse ) { extractorFields = getDefaultExtractorFields ( parserType , null ) ; } await checkStatus ( samlContent , parserType ) ; if ( checkSignature && from . entitySetting . messageSigningOrder === MessageSignatureOrder . ETS ) { const [ verified , verifiedAssertionNode ] = libsaml . verifySignature ( samlContent , verificationOptions ) ; if ( ! verified ) { return Promise . reject ( 'ERR_FAIL_TO_VERIFY_ETS_SIGNATURE' ) ; } if ( ! decryptRequired ) { extractorFields = getDefaultExtractorFields ( parserType , verifiedAssertionNode ) ; } } if ( parserType === 'SAMLResponse' && decryptRequired ) { const result = await libsaml . decryptAssertion ( self , samlContent ) ; samlContent = result [ 0 ] ; extractorFields = getDefaultExtractorFields ( parserType , result [ 1 ] ) ; } if ( checkSignature && from . entitySetting . messageSigningOrder === MessageSignatureOrder . STE ) { const [ verified , verifiedAssertionNode ] = libsaml . verifySignature ( samlContent , verificationOptions ) ; if ( verified ) { extractorFields = getDefaultExtractorFields ( parserType , verifiedAssertionNode ) ; } else { return Promise . reject ( 'ERR_FAIL_TO_VERIFY_STE_SIGNATURE' ) ; } } const parseResult = { samlContent : samlContent , extract : extract ( samlContent , extractorFields ) , } ; /**
     *  Validation part: validate the context of response after signature is verified and decrpyted (optional)
     */ const targetEntityMetadata = from . entityMeta ; const issuer = targetEntityMetadata . getEntityID ( ) ; const extractedProperties = parseResult . extract ; if ( ( parserType === 'LogoutResponse' || parserType === 'SAMLResponse' ) && extractedProperties && extractedProperties . issuer !== issuer ) { return Promise . reject ( 'ERR_UNMATCH_ISSUER' ) ; } if ( parserType === 'SAMLResponse' && extractedProperties . sessionIndex . sessionNotOnOrAfter && ! verifyTime ( undefined , extractedProperties . sessionIndex . sessionNotOnOrAfter , self . entitySetting . clockDrifts ) ) { return Promise . reject ( 'ERR_EXPIRED_SESSION' ) ; } if ( parserType === 'SAMLResponse' && extractedProperties . conditions && ! verifyTime ( extractedProperties . conditions . notBefore , extractedProperties . conditions . notOnOrAfter , self . entitySetting . clockDrifts ) ) { return Promise . reject ( 'ERR_SUBJECT_UNCONFIRMED' ) ; } return Promise . resolve ( parseResult ) ; } async function postSimpleSignFlow ( options ) { const { request , parserType , self , checkSignature = true , from } = options ; const { body , octetString } = request ; const targetEntityMetadata = from . entityMeta ; const direction = libsaml . getQueryParamByType ( parserType ) ; const encodedRequest = body [ direction ] ; const sigAlg = body [ 'SigAlg' ] ; const signature = body [ 'Signature' ] ; if ( encodedRequest === undefined ) { return Promise . reject ( 'ERR_SIMPLESIGN_FLOW_BAD_ARGS' ) ; } const xmlString = String ( base64Decode ( encodedRequest ) ) ; try { await libsaml . isValidXml ( xmlString ) ; } catch ( e ) { return Promise . reject ( 'ERR_INVALID_XML' ) ; } await checkStatus ( xmlString , parserType ) ; let assertion = '' ; if ( parserType === urlParams . samlResponse ) { const verifiedDoc = extract ( xmlString , [ { key : 'assertion' , localPath : [ '~Response' , 'Assertion' ] , attributes : [ ] , context : true } ] ) ; if ( verifiedDoc && verifiedDoc . assertion ) { assertion = verifiedDoc . assertion as string ; } } const extractorFields = getDefaultExtractorFields ( parserType , assertion . length > 0 ? assertion : null ) ; const parseResult = { samlContent : xmlString , sigAlg : null , extract : extract ( xmlString , extractorFields ) , } ; if ( checkSignature ) { if ( ! signature || ! sigAlg ) { return Promise . reject ( 'ERR_MISSING_SIG_ALG' ) ; } const base64Signature = Buffer . from ( signature , 'base64' ) ; const verified = libsaml . verifyMessageSignature ( targetEntityMetadata , octetString , base64Signature , sigAlg ) ; if ( ! verified ) { return Promise . reject ( 'ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION' ) ; } parseResult . sigAlg = sigAlg ; } /**
     *  Validation part: validate the context of response after signature is verified and decrpyted (optional)
     */ const issuer = targetEntityMetadata . getEntityID ( ) ; const extractedProperties = parseResult . extract ; if ( ( parserType === 'LogoutResponse' || parserType === 'SAMLResponse' ) && extractedProperties && extractedProperties . issuer !== issuer ) { return Promise . reject ( 'ERR_UNMATCH_ISSUER' ) ; } if ( parserType === 'SAMLResponse' && extractedProperties . sessionIndex . sessionNotOnOrAfter && ! verifyTime ( undefined , extractedProperties . sessionIndex . sessionNotOnOrAfter , self . entitySetting . clockDrifts ) ) { return Promise . reject ( 'ERR_EXPIRED_SESSION' ) ; } if ( parserType === 'SAMLResponse' && extractedProperties . conditions && ! verifyTime ( extractedProperties . conditions . notBefore , extractedProperties . conditions . notOnOrAfter , self . entitySetting . clockDrifts ) ) { return Promise . reject ( 'ERR_SUBJECT_UNCONFIRMED' ) ; } return Promise . resolve ( parseResult ) ; } function checkStatus ( content , parserType ) { if ( parserType !== urlParams . samlResponse && parserType !== urlParams . logoutResponse ) { return Promise . resolve ( 'SKIPPED' ) ; } const fields = parserType === urlParams . samlResponse ? loginResponseStatusFields : logoutResponseStatusFields ; const { top , second } = extract ( content , fields ) ; if ( top === StatusCode . Success ) { return Promise . resolve ( 'OK' ) ; } if ( ! top ) { throw new Error ( 'ERR_UNDEFINED_STATUS' ) ; } throw new Error ( `ERR_FAILED_STATUS with top tier code: ${ top }, second tier code: ${ second }` ) ; } export function flow ( options ) { const binding = options . binding ; const parserType = options . parserType ; options . supportBindings = [ BindingNamespace . Redirect , BindingNamespace . Post , BindingNamespace . SimpleSign ] ; if ( parserType === ParserType . SAMLResponse ) { options . supportBindings = [ BindingNamespace . Post , BindingNamespace . Redirect , BindingNamespace . SimpleSign ] ; } if ( binding === bindDict . post ) { return postFlow ( options ) ; } if ( binding === bindDict . redirect ) { return redirectFlow ( options ) ; } if ( binding === bindDict . simpleSign ) { return postSimpleSignFlow ( options ) ; } return Promise . reject ( 'ERR_UNEXPECTED_FLOW' ) ; }