Skip to content

Commit

Permalink
#56 Customization of AttributeStatement in template
Browse files Browse the repository at this point in the history
  • Loading branch information
tngan committed Mar 25, 2017
1 parent 784e9ee commit c6357fe
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 11 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
},
"license": "MIT",
"dependencies": {
"@types/camelcase": "^0.0.30",
"@types/lodash": "^4.14.37",
"@types/node": "^6.0.45",
"@types/node-forge": "^0.6.5",
"@types/uuid": "^2.0.29",
"@types/xmldom": "^0.1.28",
"camelcase": "^4.0.0",
"deflate-js": "^0.2.3",
"es6-promise": "^4.0.5",
"lodash": "^3.10.0",
Expand Down
2 changes: 1 addition & 1 deletion src/binding-post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async function base64LoginResponse(requestInfo: any, referenceTagXPath: string,
if (requestInfo !== null) {
tvalue.InResponseTo = requestInfo.extract.authnrequest.id;
}
rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate, tvalue);
rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue);
}
resXml = metadata.sp.isWantAssertionsSigned() ? libsaml.constructSAMLSignature(rawSamlResponse, referenceTagXPath, metadata.idp.getX509Certificate('signing'), idpSetting.privateKey, idpSetting.privateKeyPass, idpSetting.requestSignatureAlgorithm, false) : rawSamlResponse; // SS1.1 add signature algorithm
// SS-1.1
Expand Down
14 changes: 12 additions & 2 deletions src/entity-idp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,20 @@ export class IdentityProvider extends Entity {
* @param {string} meta
*/
constructor(idpSetting) {
const entitySetting = _.assign({ wantAuthnRequestsSigned: false }, idpSetting);
let entitySetting = _.assign({ wantAuthnRequestsSigned: false }, idpSetting);
// build attribute part
if (idpSetting.loginResponseTemplate) {
if(_.isString(idpSetting.loginResponseTemplate.context) && _.isArray(idpSetting.loginResponseTemplate.attributes)) {
let replacement = {
AttributeStatement: libsaml.attributeStatementBuilder(idpSetting.loginResponseTemplate.attributes)
};
entitySetting.loginResponseTemplate = libsaml.replaceTagsByValue(entitySetting.loginResponseTemplate.context, replacement);
} else {
console.warn('Invalid login response template');
}
}
super(entitySetting, 'idp');
}

/**
* @desc Generates the login response for developers to design their own method
* @param {ServiceProvider} sp object of service provider
Expand Down
53 changes: 46 additions & 7 deletions src/libsaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { pki } from 'node-forge';
import utility from './utility';
import { tags, algorithms, wording } from './urn';
import xpath, { select } from 'xpath';
import * as camel from 'camelcase';

const nrsa = require('node-rsa');
const xml = require('xml');
Expand All @@ -26,21 +27,34 @@ const dom = DOMParser;

let { SignedXml, FileKeyInfo } = require('xml-crypto');

interface ExtractorResultInterface {
interface ExtractorResult {
[key: string]: any;
signature?: any;
issuer?: string;
nameid?: string;
notexist?: boolean;
}

interface LoginResponseAttribute {
name: string;
nameFormat: string; //
valueXsiType: string; //
valueTag: string;
}

export interface LoginResponseTemplate {
context: string;
attributes?: Array<LoginResponseAttribute>;
}

export interface LibSamlInterface {
getQueryParamByType: (type: string) => string;
createXPath: (local, isExtractAll?: boolean) => string;
replaceTagsByValue: (rawXML: string, tagValues: { any }) => string;
replaceTagsByValue: (rawXML: string, tagValues: any) => string;
attributeStatementBuilder: (attributes: Array<LoginResponseAttribute>) => string;
constructSAMLSignature: (xmlString: string, referenceXPath: string, x509: string, key: string | Buffer, passphrase: string, signatureAlgorithm: string, isBase64Output?: boolean) => string;
verifySignature: (xml: string, signature, opts) => boolean;
extractor: (xmlString: string, fields) => ExtractorResultInterface;
extractor: (xmlString: string, fields) => ExtractorResult;
createKeySection: (use: string, cert: string | Buffer) => {};
constructMessageSignature: (octetString: string, key: string | Buffer, passphrase?: string, isBase64?: boolean, signingAlgorithm?: string) => string;
verifyMessageSignature: (metadata, octetString: string, signature: string | Buffer, verifyAlgorithm?: string) => boolean;
Expand All @@ -60,7 +74,7 @@ export interface LibSamlInterface {
nrsaAliasMapping: any;
defaultLoginRequestTemplate: string;
defaultLogoutRequestTemplate: string;
defaultLoginResponseTemplate: string;
defaultLoginResponseTemplate: LoginResponseTemplate;
defaultLogoutResponseTemplate: string;
}

Expand Down Expand Up @@ -100,7 +114,10 @@ const libSaml = function () {
* @desc Default login response template
* @type {String}
*/
const defaultLoginResponseTemplate = '<samlp:Response 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}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AuthnStatement}{AttributeStatement}</saml:Assertion></samlp:Response>'
const defaultLoginResponseTemplate = {
context: '<samlp:Response 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}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AuthnStatement}{AttributeStatement}</saml:Assertion></samlp:Response>',
attributes: []
};
/**
* @desc Default logout response template
* @type {String}
Expand Down Expand Up @@ -291,6 +308,17 @@ const libSaml = function () {
}
return isExtractAll === true ? "//*[local-name(.)='" + local + "']/text()" : "//*[local-name(.)='" + local + "']";
}
/**
* @private
* @desc Tag normalization
* @param {string} prefix prefix of the tag
* @param {content} content normalize it to capitalized camel case
* @return {string}
*/
function tagging(prefix: string, content: string): string {
let camelContent = camel(content);
return prefix + camelContent.charAt(0).toUpperCase() + camelContent.slice(1);
}

return {

Expand All @@ -307,13 +335,24 @@ const libSaml = function () {
* @param {array} tagValues tag values
* @return {string}
*/
replaceTagsByValue: function (rawXML: string, tagValues: { any }): string {
replaceTagsByValue: function (rawXML: string, tagValues: any): string {
Object.keys(tagValues).forEach(t => {
rawXML = rawXML.replace(new RegExp(`{${t}}`, 'g'), tagValues[t]);
});
return rawXML;
},
/**
* @desc Helper function to build the AttributeStatement tag
* @param {LoginResponseAttribute} attributes an array of attribute configuration
* @return {string}
*/
attributeStatementBuilder: function (attributes: Array<LoginResponseAttribute>): string {
const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType }) => {
return `<saml:Attribute Name="${name}" NameFormat="${nameFormat}"><saml:AttributeValue xsi:type="${valueXsiType}">{${tagging('attr', valueTag)}}</saml:AttributeValue></saml:Attribute>`
}).join('');
return `<saml:AttributeStatement>${attr}</saml:AttributeStatement>`
},
/**
* @desc Construct the XML signature for POST binding
* @param {string} xmlString request/response xml string
* @param {string} referenceXPath reference uri
Expand Down Expand Up @@ -404,7 +443,7 @@ const libSaml = function () {
meta[customKey === '' ? objKey.toLowerCase() : customKey] = res;
}
});
return <ExtractorResultInterface>meta;
return <ExtractorResult>meta;
},
/**
* @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
Expand Down
24 changes: 23 additions & 1 deletion test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,5 +457,27 @@ test('getAssertionConsumerService with two bindings', t => {
.then(res => t.pass())
.catch(err => t.fail());
});

test('building attribute statement with one attribute', t => {
const attributes = [{
name: "email",
valueTag: "user.email",
nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
valueXsiType: "xs:string"
}];
t.is(libsaml.attributeStatementBuilder(attributes), '<saml:AttributeStatement><saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xsi:type="xs:string">{attrUserEmail}</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>');
});
test('building attribute statement with multiple attributes', t => {
const attributes = [{
name: "email",
valueTag: "user.email",
nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
valueXsiType: "xs:string"
}, {
name: "firstname",
valueTag: "user.firstname",
nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
valueXsiType: "xs:string"
}];
t.is(libsaml.attributeStatementBuilder(attributes), '<saml:AttributeStatement><saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xsi:type="xs:string">{attrUserEmail}</saml:AttributeValue></saml:Attribute><saml:Attribute Name="firstname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xsi:type="xs:string">{attrUserFirstname}</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>');
});
})();
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
dependencies:
ansi-styles "^2.2.1"

"@types/camelcase@^0.0.30":
version "0.0.30"
resolved "https://registry.yarnpkg.com/@types/camelcase/-/camelcase-0.0.30.tgz#b17d7c016673bd9ebb11e1812d8732c319e5de3f"

"@types/lodash@^4.14.37":
version "4.14.37"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.37.tgz#9a2169db336c87dc289c8d0aec2e5aa707feba2b"
Expand Down Expand Up @@ -750,6 +754,10 @@ camelcase@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"

camelcase@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.0.0.tgz#8b0f90d44be5e281b903b9887349b92595ef07f2"

capture-stack-trace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
Expand Down

0 comments on commit c6357fe

Please sign in to comment.