Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ the `options` object is optional and can contain the following properties:
- `hasNonce`: adds Nonce element (default: `false`)
- `mustUnderstand`: adds mustUnderstand=1 attribute to security tag (default: `false`)
- `actor`: if set, adds Actor attribute with given value to security tag (default: `''`)
- `appendElement`: A string containing XML element to append to the end of the WSSecurity element. This can be used to add custom elements like certificates or other security tokens (default: `''`)

### WSSecurityCert

Expand Down Expand Up @@ -1061,6 +1062,7 @@ The `options` object is optional and can contain the following properties:
- `prefix`: (optional) Adds this value as a prefix for the generated signature tags.
- `attrs`: (optional) A hash of attributes and values attrName: value to add to the signature root node
- `idMode`: (optional) either 'wssecurity' to generate wsse-scoped reference Id on <Body> or undefined for an unscoped reference Id
- `appendElement`: (optional) A string containing XML element to append to the end of the WSSecurity element. This can be used to add custom elements like certificates or other security tokens.

### WSSecurityPlusCert

Expand Down Expand Up @@ -1203,6 +1205,25 @@ client.setSecurity(wsSecurityPlusCert);
</soap:Header>
```

`appendElement: '<custom:Element>test</custom:Element>'`

```xml
<soap:Header>
<wsse:Security soap:mustUnderstand="1">
<wsse:BinarySecurityToken>XXX</wsse:BinarySecurityToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
...
</SignedInfo>
</Signature>
<!-- Custom element is appended to the end of the security block -->
<custom:MyCustomElement xmlns:custom="http://example.com/custom">
foo
</custom:MyCustomElement>
</wsse:Security>
</soap:Header>
```

### WSSecurityCertWithToken

WS-Security X509 Certificate support. Just like WSSecurityCert, except that it accepts the input properties as a single object, with two properties added `username` and `password`. Which if added, will add a UsernameToken Element to the xml security element.
Expand Down
7 changes: 7 additions & 0 deletions src/security/WSSecurity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface IWSSecurityOptions {
actor?: string;
mustUnderstand?;
envelopeKey?: string;
appendElement?: string;
}

export class WSSecurity implements ISecurity {
Expand All @@ -24,12 +25,14 @@ export class WSSecurity implements ISecurity {
private _actor: string;
private _mustUnderstand: boolean;
private _envelopeKey: string;
private _appendElement: string;

constructor(username: string, password: string, options?: string | IWSSecurityOptions) {
options = options || {};
this._username = username;
this._password = password;
this._envelopeKey = 'soap';
this._appendElement = '';
// must account for backward compatibility for passwordType String param as well as object options defaults: passwordType = 'PasswordText', hasTimeStamp = true
if (typeof options === 'string') {
this._passwordType = options ? options : 'PasswordText';
Expand Down Expand Up @@ -57,6 +60,9 @@ export class WSSecurity implements ISecurity {
if (options.envelopeKey) {
this._envelopeKey = options.envelopeKey;
}
if (options.appendElement) {
this._appendElement = options.appendElement;
}
}

public toXML(): string {
Expand Down Expand Up @@ -115,6 +121,7 @@ export class WSSecurity implements ISecurity {
password +
(this._hasTokenCreated ? '<wsu:Created>' + created + '</wsu:Created>' : '') +
'</wsse:UsernameToken>' +
this._appendElement +
'</wsse:Security>'
);
}
Expand Down
9 changes: 8 additions & 1 deletion src/security/WSSecurityCert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface IWSSecurityCertOptions {
additionalReferences?: string[];
signerOptions?: IXmlSignerOptions;
excludeReferencesFromSigning?: string[];
appendElement?: string;
}

export interface IXmlSignerOptions {
Expand All @@ -78,6 +79,7 @@ export class WSSecurityCert implements ISecurity {
private expires: string;
private additionalReferences: string[] = [];
private excludeReferencesFromSigning: string[] = [];
private appendElement: string;

constructor(privatePEM: any, publicP12PEM: any, password: any, options: IWSSecurityCertOptions = {}) {
this.publicP12PEM = publicP12PEM
Expand All @@ -92,6 +94,10 @@ export class WSSecurityCert implements ISecurity {
});

this.signer.digestAlgorithm = options.digestAlgorithm ?? 'http://www.w3.org/2001/04/xmlenc#sha256';
this.appendElement = '';
if (options.appendElement) {
this.appendElement = options.appendElement;
}
if (options.signatureAlgorithm === 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256') {
this.signer.signatureAlgorithm = options.signatureAlgorithm;
this.signer.addReference({
Expand Down Expand Up @@ -155,7 +161,8 @@ export class WSSecurityCert implements ISecurity {
`EncodingType="${oasisBaseUri}/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ` +
`ValueType="${oasisBaseUri}/oasis-200401-wss-x509-token-profile-1.0#X509v3" ` +
`wsu:Id="${this.x509Id}">${this.publicP12PEM}</wsse:BinarySecurityToken>` +
timestampStr;
timestampStr +
this.appendElement;

let xmlWithSec: string;
const secExt = `xmlns:wsse="${oasisBaseUri}/oasis-200401-wss-wssecurity-secext-1.0.xsd"`;
Expand Down
8 changes: 8 additions & 0 deletions src/security/WSSecurityCertWithToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class WSSecurityCertWithToken implements ISecurity {
private additionalReferences: string[] = [];
private username: string;
private password: string;
private appendElement: string;

constructor(props: { privateKey: Buffer; publicKey: string; keyPassword?: string; username: string; password: string; options?: IWSSecurityCertOptions }) {
this.publicP12PEM = props.publicKey
Expand All @@ -75,6 +76,12 @@ export class WSSecurityCertWithToken implements ISecurity {

this.signer = new SignedXml();
const opts = props.options || {};

this.appendElement = '';
if (opts.appendElement) {
this.appendElement = opts.appendElement;
}

if (opts.signatureAlgorithm === 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256') {
this.signer.signatureAlgorithm = opts.signatureAlgorithm;
this.signer.addReference({
Expand Down Expand Up @@ -148,6 +155,7 @@ export class WSSecurityCertWithToken implements ISecurity {
`wsu:Id="${this.x509Id}">${this.publicP12PEM}</wsse:BinarySecurityToken>` +
usernameToken +
timestampStr +
this.appendElement +
`</wsse:Security>`;

const xmlWithSec = insertStr(secHeader, xml, xml.indexOf(`</${envelopeKey}:Header>`));
Expand Down
70 changes: 49 additions & 21 deletions test/security/WSSecurity.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
'use strict';

var fs = require('fs'),
join = require('path').join;
const { equal } = require('should');
const sinon = require('sinon');

describe('WSSecurity', function () {
var WSSecurity = require('../../').WSSecurity;

let clock;

before(() => {
const fixedDate = new Date('2025-10-06T00:00:00Z');
clock = sinon.useFakeTimers(fixedDate.getTime());
});

after(() => {
clock.restore();
});

it('is a function', function () {
WSSecurity.should.be.type('function');
});
Expand Down Expand Up @@ -43,31 +54,26 @@ describe('WSSecurity', function () {
var password = 'my&Pass';
var options = {
passwordType: 'PassWordText',
hasNonce: true,
hasNonce: false,
actor: 'urn:sample',
};
var instance = new WSSecurity(username, password, options);
var xml = instance.toXML();

xml.should.containEql('<wsse:Security soap:actor="urn:sample" ');
xml.should.containEql('xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ');
xml.should.containEql('xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">');
xml.should.containEql('<wsu:Timestamp wsu:Id="Timestamp-');
xml.should.containEql('<wsu:Created>');
xml.should.containEql('<wsu:Expires>');
xml.should.containEql('</wsu:Timestamp>');
xml.should.containEql('<wsse:UsernameToken ');
xml.should.containEql('xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" ');
xml.should.containEql('wsu:Id="SecurityToken-');
xml.should.containEql('<wsse:Username>my&amp;User</wsse:Username>');
xml.should.containEql('<wsse:Password ');
xml.should.containEql('Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">');
xml.should.containEql('my&amp;Pass</wsse:Password>');
xml.should.containEql('<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">');
xml.should.containEql('</wsse:Nonce>');
xml.should.containEql('<wsu:Created>');
xml.should.containEql('</wsse:UsernameToken></wsse:Security>');
equal(
xml,
`<wsse:Security soap:actor="urn:sample" 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">` +
`<wsu:Timestamp wsu:Id="Timestamp-2025-10-06T00:00:00Z">` +
`<wsu:Created>2025-10-06T00:00:00Z</wsu:Created>` +
`<wsu:Expires>2025-10-06T00:10:00Z</wsu:Expires>` +
`</wsu:Timestamp><wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-2025-10-06T00:00:00Z">` +
`<wsse:Username>my&amp;User</wsse:Username>` +
`<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">my&amp;Pass</wsse:Password>` +
`<wsu:Created>2025-10-06T00:00:00Z</wsu:Created>` +
`</wsse:UsernameToken></wsse:Security>`,
);
});

it('should add envelopeKey to properties in Security block', function () {
var username = 'myUser';
var password = 'myPass';
Expand All @@ -82,4 +88,26 @@ describe('WSSecurity', function () {
xml.should.containEql('<wsse:Security soapenv:actor="urn:sample" ');
xml.should.containEql('soapenv:mustUnderstand="1"');
});

it('should add appendElement when provided', function () {
var username = 'myUser';
var password = 'myPass';
var options = {
hasTimeStamp: false,
appendElement: '<custom:MyCustomElement xmlns:custom="http://example.com/custom">foo</custom:MyCustomElement>',
};
var instance = new WSSecurity(username, password, options);
var xml = instance.toXML();

equal(
xml,
`<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">` +
`<wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-2025-10-06T00:00:00Z">` +
`<wsse:Username>myUser</wsse:Username>` +
`<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">myPass</wsse:Password>` +
`<wsu:Created>2025-10-06T00:00:00Z</wsu:Created>` +
`</wsse:UsernameToken>` +
`<custom:MyCustomElement xmlns:custom="http://example.com/custom">foo</custom:MyCustomElement>` +
`</wsse:Security>`)
});
});
29 changes: 29 additions & 0 deletions test/security/WSSecurityCert.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,33 @@ describe('WSSecurityCert', function () {
var xml = instance.postProcess('<soap:Envelope><soap:Header></soap:Header><soap:Body><Body></Body></soap:Body></soap:Envelope>', 'soap');
xml.should.containEql('DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"');
});

it('should add appendElement when provided', function () {
var instance = new WSSecurityCert(key, cert, '', {
appendElement: '<Certificate>Mfg...1+</Certificate>',
});
var xml = instance.postProcess('<soap:Envelope><soap:Header></soap:Header><soap:Body></soap:Body></soap:Envelope>', 'soap');

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="1"');
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('<Certificate>Mfg...1+</Certificate>');
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('<KeyInfo>');
xml.should.containEql('</KeyInfo>');
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());
});
});
25 changes: 25 additions & 0 deletions test/security/WSSecurityCertWithToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,5 +266,30 @@ describe('WSSecurityCertWithToken', function () {
}
should(xml).not.be.ok();
});

it('should add appendElement when provided', function () {
var instance = new WSSecurityCertWithToken({ privateKey: key, publicKey: cert, keyPassword: '', options: { appendElement: '<custom:MyCustomElement xmlns:custom="http://example.com/custom">foo</custom:MyCustomElement>' } });
var xml = instance.postProcess('<soap:Envelope><soap:Header></soap:Header><soap:Body></soap:Body></soap:Envelope>', 'soap');

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="1"');
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('<custom:MyCustomElement xmlns:custom="http://example.com/custom">foo</custom:MyCustomElement>');
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());
});
});
});
Loading