Skip to content

Commit

Permalink
Add testing cases for login flow control and catch bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
tngan committed Jun 10, 2017
1 parent 36ea812 commit 4256b68
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 15 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
],
"scripts": {
"build": "npm run lint && tsc",
"build:nolint": "tsc",
"lint": "tslint --project tsconfig.json",
"lint:fix": "tslint --fix --project tsconfig.json",
"pretest": "make pretest",
"test": "npm run build && nyc ava --verbose build/test",
"test:pure": "nyc ava --verbose build/test",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"postinstall": "node postinstall"
},
Expand Down
14 changes: 5 additions & 9 deletions src/binding-post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@ function base64LoginRequest(referenceTagXPath: string, entity: any, customTagRep
if (metadata && metadata.idp && metadata.sp) {
const base = metadata.idp.getSingleSignOnService(binding.post);
let rawSamlRequest;
if (metadata.sp.isAuthnRequestSigned() !== metadata.idp.isWantAuthnRequestsSigned()) {
throw new Error('Conflict of metadata - sp isAuthnRequestSigned is not equal to idp isWantAuthnRequestsSigned');
}
if (spSetting.loginRequestTemplate) {
const info = customTagReplacement(spSetting.loginRequestTemplate);
const info = customTagReplacement(spSetting.loginRequestTemplate.context);
id = get<string>(info, 'id');
rawSamlRequest = get<string>(info, 'context');
} else {
Expand Down Expand Up @@ -67,7 +64,7 @@ function base64LoginRequest(referenceTagXPath: string, entity: any, customTagRep
context: utility.base64Encode(rawSamlRequest),
};
}
throw new Error('Missing declaration of metadata');
throw new Error('missing declaration of metadata');
}
/**
* @desc Generate a base64 encoded login response
Expand Down Expand Up @@ -115,7 +112,7 @@ async function base64LoginResponse(requestInfo: any, entity: any, user: any = {}
AttributeStatement: '',
};
if (idpSetting.loginResponseTemplate) {
const template = customTagReplacement(idpSetting.loginResponseTemplate);
const template = customTagReplacement(idpSetting.loginResponseTemplate.context);
id = get<string>(template, 'id');
rawSamlResponse = get<string>(template, 'context');
} else {
Expand All @@ -132,7 +129,6 @@ async function base64LoginResponse(requestInfo: any, entity: any, user: any = {}
signingCert: metadata.idp.getX509Certificate('signing'),
isBase64Output: false,
};

// SAML response must be signed
if (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned()) {
rawSamlResponse = libsaml.constructSAMLSignature({
Expand Down Expand Up @@ -186,7 +182,7 @@ function base64LogoutRequest(user, referenceTagXPath, entity, customTagReplaceme
if (metadata && metadata.init && metadata.target) {
let rawSamlRequest;
if (initSetting.loginRequestTemplate) {
const template = customTagReplacement(initSetting.loginRequestTemplate);
const template = customTagReplacement(initSetting.loginRequestTemplate.context);
id = get<string>(template, 'id');
rawSamlRequest = get<string>(template, 'context');
} else {
Expand Down Expand Up @@ -241,7 +237,7 @@ function base64LogoutResponse(requestInfo: any, entity: any, customTagReplacemen
if (metadata && metadata.init && metadata.target) {
let rawSamlResponse;
if (initSetting.logoutResponseTemplate) {
const template = customTagReplacement(initSetting.logoutResponseTemplate);
const template = customTagReplacement(initSetting.logoutResponseTemplate.context);
id = template.id;
rawSamlResponse = template.context;
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/binding-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function buildRedirectURL(opts: BuildRedirectConfig) {
entitySetting,
} = opts;
let {relayState = '' } = opts;
const noParams = url.parse(baseUrl).query.length === 0;
const noParams = (url.parse(baseUrl).query || []).length === 0;
const queryParam = libsaml.getQueryParamByType(type);
// In general, this xmlstring is required to do deflate -> base64 -> urlencode
const samlRequest = encodeURIComponent(utility.base64Encode(utility.deflateString(context)));
Expand Down Expand Up @@ -84,7 +84,7 @@ function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacem
const base = metadata.idp.getSingleSignOnService(binding.redirect);
let rawSamlRequest: string;
if (spSetting.loginRequestTemplate) {
const info = customTagReplacement(spSetting.logoutRequestTemplate);
const info = customTagReplacement(spSetting.loginRequestTemplate);
id = get<string>(info, 'id');
rawSamlRequest = get<string>(info, 'context');
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/entity-idp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class IdentityProvider extends Entity {
* @param {object} user current logged user (e.g. req.user)
* @param {function} customTagReplacement used when developers have their own login response template
*/
public async createLoginResponse(sp, requestInfo, binding, user, customTagReplacement) {
public async createLoginResponse(sp, requestInfo, binding, user, customTagReplacement?) {
const protocol = namespace.binding[binding] || namespace.binding.redirect;
if (protocol === namespace.binding.post) {
const context = await postBinding.base64LoginResponse(requestInfo, {
Expand All @@ -91,7 +91,7 @@ export class IdentityProvider extends Entity {

} else {
// Will support artifact in the next release
throw new Error('This binding is not support');
throw new Error('this binding is not supported');
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/entity-sp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ export class ServiceProvider extends Entity {
* @param {string} binding protocol binding
* @param {function} customTagReplacement used when developers have their own login response template
*/
public createLoginRequest(idp, binding = 'redirect', customTagReplacement): BindingContext | PostRequestInfo {
public createLoginRequest(idp, binding = 'redirect', customTagReplacement?): BindingContext | PostRequestInfo {
const nsBinding = namespace.binding;
const protocol = nsBinding[binding];
if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) {
throw new Error('metadata conflict - sp isAuthnRequestSigned is not equal to idp isWantAuthnRequestsSigned');
}
if (protocol === nsBinding.redirect) {
return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement);
} else if (protocol === nsBinding.post) {
Expand Down
137 changes: 137 additions & 0 deletions test/flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import esaml2 = require('../index');
import { readFileSync, writeFileSync } from 'fs';
import test from 'ava';
import { assign } from 'lodash';
import xpath from 'xpath';
import { DOMParser as dom } from 'xmldom';
import { xpath as select } from 'xml-crypto';
import * as _ from 'lodash';
import { PostRequestInfo } from '../src/entity';

const {
IdentityProvider: identityProvider,
ServiceProvider: serviceProvider,
IdPMetadata: idpMetadata,
SPMetadata: spMetadata,
Utility: utility,
SamlLib: libsaml,
Constants: ref,
} = esaml2;

const getQueryParamByType = libsaml.getQueryParamByType;
const binding = ref.namespace.binding;
const algorithms = ref.algorithms;
const wording = ref.wording;
const signatureAlgorithms = algorithms.signature;

// Define of metadata
const _spKeyFolder = './test/key/sp/';
const _spPrivPem = String(readFileSync(_spKeyFolder + 'privkey.pem'));
const _spPrivKey = _spKeyFolder + 'nocrypt.pem';
const _spPrivKeyPass = 'VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px';

const defaultIdpConfig = {
privateKey: readFileSync('./test/key/idp/privkey.pem'),
privateKeyPass: 'q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW',
isAssertionEncrypted: true,
encPrivateKey: readFileSync('./test/key/idp/encryptKey.pem'),
encPrivateKeyPass: 'g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN',
metadata: readFileSync('./test/misc/IDPMetadata.xml'),
};

const defaultSpConfig = {
privateKey: readFileSync('./test/key/sp/privkey.pem'),
privateKeyPass: 'VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px',
isAssertionEncrypted: true, // for logout purpose
encPrivateKey: readFileSync('./test/key/sp/encryptKey.pem'),
encPrivateKeyPass: 'BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU',
metadata: readFileSync('./test/misc/SPMetadata.xml'),
};

// Define an identity provider
const idp = identityProvider(defaultIdpConfig);
const sp = serviceProvider(defaultSpConfig);

// Define metadata
const IdPMetadata = idpMetadata(readFileSync('./test/misc/IDPMetadata.xml'));
const SPMetadata = spMetadata(readFileSync('./test/misc/SPMetadata.xml'));
const sampleSignedResponse = readFileSync('./test/misc/SignSAMLResponse.xml').toString();
const wrongResponse = readFileSync('./test/misc/wrongResponse.xml').toString();
const spCertKnownGood = readFileSync('./test/key/sp/knownGoodCert.cer').toString().trim();
const spPemKnownGood = readFileSync('./test/key/sp/knownGoodEncryptKey.pem').toString().trim();
const noSignedIdpMetadata = readFileSync('./test/misc/NoSignIDPMetadata.xml').toString().trim();

function writer(str) {
writeFileSync('test.txt', str);
}

test('create login request with redirect binding using default template', t => {
const { id, context } = sp.createLoginRequest(idp, 'redirect');
_.isString(id) && _.isString(context) ? t.pass() : t.fail();
});

test('create login request with post binding using default template', t => {
const { relayState, type, entityEndpoint, id, context } = sp.createLoginRequest(idp, 'post') as PostRequestInfo;
_.isString(id) && _.isString(context) && _.isString(entityEndpoint) && _.isEqual(type, 'SAMLRequest') ? t.pass() : t.fail();
});

test('signed in sp is not matched with the signed notation in idp with post request', t => {
const _idp = identityProvider({ ...defaultIdpConfig, metadata: noSignedIdpMetadata });
try {
const { id, context } = sp.createLoginRequest(_idp, 'post');
t.fail();
} catch (e) {
t.is(e.message, 'metadata conflict - sp isAuthnRequestSigned is not equal to idp isWantAuthnRequestsSigned');
}
});

test('signed in sp is not matched with the signed notation in idp with redirect request', t => {
const _idp = identityProvider({ ...defaultIdpConfig, metadata: noSignedIdpMetadata });
try {
const { id, context } = sp.createLoginRequest(_idp, 'redirect');
t.fail();
} catch (e) {
t.is(e.message, 'metadata conflict - sp isAuthnRequestSigned is not equal to idp isWantAuthnRequestsSigned');
}
});

test('create login request with redirect binding using custom template', t => {
const _sp = serviceProvider({ ...defaultSpConfig, loginRequestTemplate: {
context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
}});
const { id, context } = _sp.createLoginRequest(idp, 'redirect', template => {
return {
id: 'exposed_testing_id',
context: template, // all the tags are supposed to be replaced
};
});
(id === 'exposed_testing_id' && _.isString(context)) ? t.pass() : t.fail();
});

test('create login request with post binding using custom template', t => {
const _sp = serviceProvider({ ...defaultSpConfig, loginRequestTemplate: {
context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
}});
const { id, context, entityEndpoint, type, relayState } = _sp.createLoginRequest(idp, 'post', template => {
return {
id: 'exposed_testing_id',
context: template, // all the tags are supposed to be replaced
};
}) as PostRequestInfo;
id === 'exposed_testing_id' &&
_.isString(context) &&
_.isString(relayState) &&
_.isString(entityEndpoint) &&
_.isEqual(type, 'SAMLRequest')
? t.pass() : t.fail();
});

test('create login response with undefined binding', async t => {
const error = await t.throws(idp.createLoginResponse(sp, {}, 'undefined', { email: 'user@esaml2.com' }));
t.is(error.message, 'this binding is not supported');
});

test('create post login response', async t => {
const { id, context } = await idp.createLoginResponse(sp, null, 'post', { email: 'user@esaml2.com' });
_.isString(id) && _.isString(context) ? t.pass() : t.fail();
});
1 change: 1 addition & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { assign } from 'lodash';
import xpath from 'xpath';
import { DOMParser as dom } from 'xmldom';
import { xpath as select } from 'xml-crypto';
import * as _ from 'lodash';

const {
IdentityProvider: identityProvider,
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"sourceMap": true,
"outDir": "./build",
"baseUrl": "./",
"removeComments": false,
"paths": {},
"lib": [
"dom",
Expand Down
2 changes: 1 addition & 1 deletion tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"jsdoc-format": false,
"max-line-length": false,
"member-access": false,
"no-console": [true, "log"],
"no-console": [false],
"no-consecutive-blank-lines": [true, 3],
"no-empty-interface": false,
"no-string-literal": false,
Expand Down

0 comments on commit 4256b68

Please sign in to comment.