Skip to content

Commit

Permalink
Added support for WSSecurity XML signing with x509 certificates.
Browse files Browse the repository at this point in the history
updated readme
updated package.json with latest ursa version
change minimum node requirement to 0.10 since many dependencies break with engine-strict=true under node 0.8
  • Loading branch information
Jon Ciccone authored and CT Arrington committed Feb 25, 2016
1 parent 34d42a0 commit 06d90f7
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 5 deletions.
11 changes: 9 additions & 2 deletions .travis.yml
Expand Up @@ -3,13 +3,20 @@ language: node_js
notifications:
email: false
node_js:
- 0.8
- 0.10
- 0.12
- 4.0
- iojs
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_install:
- npm install -g npm@~1.4.6
- npm -g install npm@latest
script:
- npm run cover
- npm run coveralls
10 changes: 10 additions & 0 deletions Readme.md
Expand Up @@ -321,6 +321,16 @@ as default request options to the constructor:
client.setSecurity(new soap.WSSecurity('username', 'password'))
```

####WSSecurity with X509 Certificate

``` javascript
var privateKey = fs.readFileSync(privateKeyPath);
var publicKey = fs.readFileSync(publicKeyPath);
var password = ''; // optional password
var wsSecurity = new soap.WSSecurityCert(privateKey, publicKey, password, 'utf8');
client.setSecurity(wsSecurity);
```

####BearerSecurity

``` javascript
Expand Down
7 changes: 6 additions & 1 deletion lib/client.js
Expand Up @@ -226,19 +226,24 @@ Client.prototype._invoke = function(method, args, location, callback, options, e
(
"<soap:Header>" +
(self.soapHeaders ? self.soapHeaders.join("\n") : "") +
(self.security ? self.security.toXML() : "") +
(self.security && !self.security.postProcess ? self.security.toXML() : "") +
"</soap:Header>"
)
:
''
) +
"<soap:Body" +
(self.bodyAttributes ? self.bodyAttributes.join(' ') : '') +
(self.security && self.security.postProcess ? " Id='_0'" : '') +
">" +
message +
"</soap:Body>" +
"</soap:Envelope>";

if(self.security && self.security.postProcess){
xml = self.security.postProcess(xml);
}

self.lastMessage = message;
self.lastRequest = xml;
self.lastEndpoint = location;
Expand Down
78 changes: 78 additions & 0 deletions lib/security/WSSecurityCert.js
@@ -0,0 +1,78 @@
"use strict";

var ursa = require('ursa');
var fs = require('fs');
var path = require('path');
var ejs = require('ejs');
var SignedXml = require('xml-crypto').SignedXml;
var uuid = require('node-uuid');
var wsseSecurityHeaderTemplate = ejs.compile(fs.readFileSync(path.join(__dirname, 'templates', 'wsse-security-header.ejs')).toString());
var wsseSecurityTokenTemplate = ejs.compile(fs.readFileSync(path.join(__dirname, 'templates', 'wsse-security-token.ejs')).toString());

function addMinutes(date, minutes) {
return new Date(date.getTime() + minutes * 60000);
}

function dateStringForSOAP(date) {
return date.getUTCFullYear() + '-' + ('0' + (date.getUTCMonth() + 1)).slice(-2) + '-' +
('0' + date.getUTCDate()).slice(-2) + 'T' + ('0' + date.getUTCHours()).slice(-2) + ":" +
('0' + date.getUTCMinutes()).slice(-2) + ":" + ('0' + date.getUTCSeconds()).slice(-2) + "Z";
}

function generateCreated() {
return dateStringForSOAP(new Date());
}

function generateExpires() {
return dateStringForSOAP(addMinutes(new Date(), 10));
}

function insertStr(src, dst, pos) {
return [dst.slice(0, pos), src, dst.slice(pos)].join('');
}

function generateId() {
return uuid.v4().replace(/-/gm, '');
}

function WSSecurityCert(privatePEM, publicP12PEM, password, encoding) {

this.privateKey = ursa.createPrivateKey(privatePEM, password, encoding);
this.publicP12PEM = publicP12PEM.toString().replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----', '').replace(/(\r\n|\n|\r)/gm, '');

this.signer = new SignedXml();
this.signer.signingKey = this.privateKey.toPrivatePem();
this.x509Id = "x509-" + generateId();

var references = ["http://www.w3.org/2000/09/xmldsig#enveloped-signature",
"http://www.w3.org/2001/10/xml-exc-c14n#"];

this.signer.addReference("//*[local-name(.)='Body']", references);
this.signer.addReference("//*[local-name(.)='Timestamp']", references);

var _this = this;
this.signer.keyInfoProvider = {};
this.signer.keyInfoProvider.getKeyInfo = function (key) {
return wsseSecurityTokenTemplate({ x509Id: _this.x509Id });
};
}

WSSecurityCert.prototype.postProcess = function (xml) {
this.created = generateCreated();
this.expires = generateExpires();

var secHeader = wsseSecurityHeaderTemplate({
binaryToken: this.publicP12PEM,
created: this.created,
expires: this.expires,
id: this.x509Id
});

var xmlWithSec = insertStr(secHeader, xml, xml.indexOf('</soap:Header>'));

this.signer.computeSignature(xmlWithSec);

return insertStr(this.signer.getSignatureXml(), xmlWithSec, xmlWithSec.indexOf('</wsse:Security>'));
};

module.exports = WSSecurityCert;
1 change: 1 addition & 0 deletions lib/security/index.js
Expand Up @@ -6,4 +6,5 @@ module.exports = {
, ClientSSLSecurityPFX: require('./ClientSSLSecurityPFX')
, WSSecurity: require('./WSSecurity')
, BearerSecurity: require('./BearerSecurity')
, WSSecurityCert: require('./WSSecurityCert')
};
12 changes: 12 additions & 0 deletions lib/security/templates/wsse-security-header.ejs
@@ -0,0 +1,12 @@
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
soap:mustUnderstand="true">
<wsse:BinarySecurityToken
EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
wsu:Id="<%-id%>"><%-binaryToken%></wsse:BinarySecurityToken>
<Timestamp xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" Id="_1">
<Created><%-created%></Created>
<Expires><%-expires%></Expires>
</Timestamp>
</wsse:Security>
3 changes: 3 additions & 0 deletions lib/security/templates/wsse-security-token.ejs
@@ -0,0 +1,3 @@
<wsse:SecurityTokenReference>
<wsse:Reference URI="<%-x509Id%>" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
</wsse:SecurityTokenReference>
1 change: 1 addition & 0 deletions lib/soap.js
Expand Up @@ -69,6 +69,7 @@ function listen(server, pathOrOptions, services, xml) {
exports.security = security;
exports.BasicAuthSecurity = security.BasicAuthSecurity;
exports.WSSecurity = security.WSSecurity;
exports.WSSecurityCert = security.WSSecurityCert;
exports.ClientSSLSecurity = security.ClientSSLSecurity;
exports.ClientSSLSecurityPFX = security.ClientSSLSecurityPFX;
exports.BearerSecurity = security.BearerSecurity;
Expand Down
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -3,7 +3,7 @@
"version": "0.13.0",
"description": "A minimal node SOAP client",
"engines": {
"node": ">=0.8.0"
"node": ">=0.10.0"
},
"author": "Vinay Pulim <v@pulim.com>",
"dependencies": {
Expand All @@ -12,7 +12,11 @@
"request": ">=2.9.0",
"sax": ">=0.6",
"selectn": "^0.9.6",
"strip-bom": "~0.3.1"
"strip-bom": "~0.3.1",
"ursa": "0.8.5 || >=0.9.3",
"node-uuid": "~1.4.3",
"ejs": "~2.3.4",
"xml-crypto": "~0.8.0"
},
"repository": {
"type": "git",
Expand Down
72 changes: 72 additions & 0 deletions test/security/WSSecurityCert.js
@@ -0,0 +1,72 @@
'use strict';

var fs = require('fs'),
join = require('path').join;

describe('WSSecurityCert', function() {
var WSSecurityCert = require('../../').WSSecurityCert;
var cert = fs.readFileSync(join(__dirname, '..', 'certs', 'agent2-cert.pem'));
var key = fs.readFileSync(join(__dirname, '..', 'certs', 'agent2-key.pem'));

it('is a function', function() {
WSSecurityCert.should.be.type('function');
});

it('should accept valid constructor variables', function() {
var instance = new WSSecurityCert(key, cert, '', 'utf8');
instance.should.have.property('privateKey');
instance.should.have.property('publicP12PEM');
instance.should.have.property('signer');
instance.should.have.property('x509Id');
});

it('should not accept invalid constructor variables', function() {
var passed = true;

try {
new WSSecurityCert('*****', cert, '', 'utf8');
} catch(e) {
passed = false;
}

if (passed) {
throw new Error('bad private key');
}

passed = true;

try {
new WSSecurityCert(key, cert, '', 'bob');
} catch(e) {
passed = false;
}

if (passed) {
throw new Error('bad encoding');
}
});

it('should insert a WSSecurity signing block when postProcess is called', function() {
var instance = new WSSecurityCert(key, cert, '', 'utf8');
var xml = instance.postProcess('<soap:Header></soap:Header><soap:Body></soap:Body>');

xml.should.containEql('<wsse:Security');
xml.should.containEql('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd');
xml.should.containEql('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd');
xml.should.containEql('soap:mustUnderstand="true"');
xml.should.containEql('<wsse:BinarySecurityToken');
xml.should.containEql('EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary');
xml.should.containEql('ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"');
xml.should.containEql('wsu:Id="' + instance.x509Id);
xml.should.containEql('</wsse:BinarySecurityToken>');
xml.should.containEql('<Timestamp xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" Id="_1">');
xml.should.containEql('<Created>' + instance.created);
xml.should.containEql('<Expires>' + instance.expires);
xml.should.containEql('<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">');
xml.should.containEql('<wsse:SecurityTokenReference>');
xml.should.containEql('<wsse:Reference URI="' + instance.x509Id);
xml.should.containEql('ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>');
xml.should.containEql(instance.publicP12PEM);
xml.should.containEql(instance.signer.getSignatureXml());
});
});

0 comments on commit 06d90f7

Please sign in to comment.