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: ParserType, assertion?: any): ExtractorFields { 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): Promise<FlowResult> {
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: string = '';
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: string, extract: any, sigAlg: (string | null) } = { 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; }
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): Promise<FlowResult> {
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: 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), };
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): Promise<FlowResult> {
const { request, parserType, self, checkSignature = true, from } = options;
const { body, octetString } = request;
const targetEntityMetadata = from.entityMeta;
const direction = libsaml.getQueryParamByType(parserType); const encodedRequest: string = body[direction]; const sigAlg: string = body['SigAlg']; const signature: string = 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: string = '';
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: string, extract: any, sigAlg: (string | null) } = { 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; }
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: string, parserType: string): Promise<string> {
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): Promise<FlowResult> {
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');
}