Skip to content

Commit

Permalink
#279 Returns detailed message for failed status code and support two-…
Browse files Browse the repository at this point in the history
…tiers status code (#286)
  • Loading branch information
tngan committed Jun 25, 2019
1 parent b1956d3 commit a0ab7ca
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 13 deletions.
34 changes: 26 additions & 8 deletions src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const dom = DOMParser;

interface ExtractorField {
key: string;
localPath: string[];
localPath: string[] | string[][];
attributes: string[];
index?: string[];
attributePath?: string[];
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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'],
Expand Down
47 changes: 42 additions & 5 deletions src/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ||
Expand All @@ -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) {
Expand Down Expand Up @@ -140,9 +146,12 @@ async function postFlow(options): Promise<FlowResult> {
// 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 (
Expand Down Expand Up @@ -182,7 +191,9 @@ async function postFlow(options): Promise<FlowResult> {
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;
Expand Down Expand Up @@ -223,6 +234,32 @@ async function postFlow(options): Promise<FlowResult> {
return Promise.resolve(parseResult);
}

function checkStatus(content: string, parserType: string): Promise<string> {

// 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<FlowResult> {

const binding = options.binding;
Expand Down
2 changes: 2 additions & 0 deletions src/urn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions test/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
1 change: 1 addition & 0 deletions test/misc/failed_response.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="_41e758fee373d51639552c4b040b1090e97f6685"><saml:Issuer>https://idp.example.com/metadata</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Requester"><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy"/></samlp:StatusCode></samlp:Status></samlp:Response>

0 comments on commit a0ab7ca

Please sign in to comment.