diff --git a/src/extractor.ts b/src/extractor.ts index 5b14057f..02c0f6a6 100644 --- a/src/extractor.ts +++ b/src/extractor.ts @@ -6,7 +6,7 @@ const dom = DOMParser; interface ExtractorField { key: string; - localPath: string[]; + localPath: string[] | string[][]; attributes: string[]; index?: string[]; attributePath?: string[]; @@ -70,12 +70,35 @@ export const loginRequestFields: ExtractorFields = [ } ]; -export const loginResponseFields: ((asserion: any) => ExtractorFields) = assertion => [ +// support two-tiers status code +export const loginResponseStatusFields = [ { - key: 'statusCode', + key: 'top', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'], }, + { + key: 'second', + localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'], + attributes: ['Value'], + } +]; + +// support two-tiers status code +export const logoutResponseStatusFields = [ + { + key: 'top', + localPath: ['LogoutResponse', 'Status', 'StatusCode'], + attributes: ['Value'] + }, + { + key: 'second', + localPath: ['LogoutResponse', 'Status', 'StatusCode', 'StatusCode'], + attributes: ['Value'], + } +]; + +export const loginResponseFields: ((asserion: any) => ExtractorFields) = assertion => [ { key: 'conditions', localPath: ['Assertion', 'Conditions'], @@ -156,11 +179,6 @@ export const logoutResponseFields: ExtractorFields = [ localPath: ['LogoutResponse'], attributes: ['ID', 'Destination', 'InResponseTo'] }, - { - key: 'statusCode', - localPath: ['LogoutResponse', 'Status', 'StatusCode'], - attributes: ['Value'] - }, { key: 'issuer', localPath: ['LogoutResponse', 'Issuer'], diff --git a/src/flow.ts b/src/flow.ts index bd9ed909..1a04bfc2 100644 --- a/src/flow.ts +++ b/src/flow.ts @@ -7,14 +7,17 @@ import { loginResponseFields, logoutRequestFields, logoutResponseFields, - ExtractorFields + ExtractorFields, + logoutResponseStatusFields, + loginResponseStatusFields } from './extractor'; import { BindingNamespace, ParserType, wording, - MessageSignatureOrder + MessageSignatureOrder, + StatusCode } from './urn'; const bindDict = wording.binding; @@ -65,7 +68,7 @@ async function redirectFlow(options) { const xmlString = inflateString(decodeURIComponent(content)); - // validate the response xml + // validate the xml (remarks: login response must be gone through post flow) if ( parserType === urlParams.samlRequest || parserType === urlParams.logoutRequest || @@ -86,6 +89,9 @@ async function redirectFlow(options) { extract: extract(xmlString, extractorFields), }; + // check status based on different scenarios + await checkStatus(xmlString, parserType); + // see if signature check is required // only verify message signature is enough if (checkSignature) { @@ -140,9 +146,12 @@ async function postFlow(options): Promise { // validate the xml first await libsaml.isValidXml(samlContent); - if (parserType !== 'SAMLResponse') { + if (parserType !== urlParams.samlResponse) { extractorFields = getDefaultExtractorFields(parserType, null); } + + // check status based on different scenarios + await checkStatus(samlContent, parserType); // verify the signatures (the repsonse is encrypted then signed, then verify first then decrypt) if ( @@ -182,7 +191,9 @@ async function postFlow(options): Promise { extract: extract(samlContent, extractorFields), }; - // validation part + /** + * 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; @@ -223,6 +234,32 @@ async function postFlow(options): Promise { return Promise.resolve(parseResult); } +function checkStatus(content: string, parserType: string): Promise { + + // only check response parser + if (parserType !== urlParams.samlResponse && parserType !== urlParams.logoutResponse) { + return Promise.resolve('SKIPPED'); + } + + const fields = parserType === urlParams.samlResponse + ? loginResponseStatusFields + : logoutResponseStatusFields; + + const {top, second} = extract(content, fields); + + // only resolve when top-tier status code is success + if (top === StatusCode.Success) { + return Promise.resolve('OK'); + } + + if (!top) { + throw new Error('ERR_UNDEFINED_STATUS'); + } + + // returns a detailed error for two-tier error code + throw new Error(`ERR_FAILED_STATUS with top tier code: ${top}, second tier code: ${second}`); +} + export function flow(options): Promise { const binding = options.binding; diff --git a/src/urn.ts b/src/urn.ts index 99c5c57b..0e421c74 100644 --- a/src/urn.ts +++ b/src/urn.ts @@ -16,10 +16,12 @@ export enum MessageSignatureOrder { } export enum StatusCode { + // top-tier Success = 'urn:oasis:names:tc:SAML:2.0:status:Success', Requester = 'urn:oasis:names:tc:SAML:2.0:status:Requester', Responder = 'urn:oasis:names:tc:SAML:2.0:status:Responder', VersionMismatch = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch', + // second-tier to provide more information AuthFailed = 'urn:oasis:names:tc:SAML:2.0:status:AuthnFailed', InvalidAttrNameOrValue = 'urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue', InvalidNameIDPolicy = 'urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy', diff --git a/test/flow.ts b/test/flow.ts index dc66b701..446e712f 100644 --- a/test/flow.ts +++ b/test/flow.ts @@ -651,3 +651,12 @@ test('should reject signature wrapped response - case 2', async t => { t.is(e.message, 'ERR_POTENTIAL_WRAPPING_ATTACK'); } }); + +test('should throw two-tiers code error when the response does not return success status', async t => { + const failedResponse: string = String(readFileSync('./test/misc/failed_response.xml')); + try { + const _result = await sp.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse: utility.base64Encode(failedResponse) } }); + } catch (e) { + t.is(e.message, 'ERR_FAILED_STATUS with top tier code: urn:oasis:names:tc:SAML:2.0:status:Requester, second tier code: urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy'); + } +}); \ No newline at end of file diff --git a/test/misc/failed_response.xml b/test/misc/failed_response.xml new file mode 100644 index 00000000..67b258cd --- /dev/null +++ b/test/misc/failed_response.xml @@ -0,0 +1 @@ +https://idp.example.com/metadata \ No newline at end of file