Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WSSecurity Protocol with user/pass token element. #1187

Merged
merged 18 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ This module lets you connect to web services using SOAP. It also provides a ser
- [ClientSSLSecurityPFX](#clientsslsecuritypfx)
- [WSSecurity](#wssecurity)
- [WSSecurityCert](#wssecuritycert)
- [WSSecurityPlusCert](#wssecuritypluscert)
- [WSSecurityCertWithToken](#wssecuritycertwithtoken)
- [NTLMSecurity](#ntlmsecurity)
- [Handling XML Attributes, Value and XML (wsdlOptions).](#handling-xml-attributes-value-and-xml-wsdloptions)
- [Overriding the `value` key](#overriding-the-value-key)
Expand Down Expand Up @@ -1081,6 +1083,16 @@ Use WSSecurity and WSSecurityCert together.
</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.

``` xml
<wsse:UsernameToken>
<wsse:Username>someusername</wsse:Username>
<wsse:Password>someusername's password</wsse:Password>
</wsse:UsernameToken>
```

### NTLMSecurity

Expand Down
167 changes: 167 additions & 0 deletions src/security/WSSecurityCertWithToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { v4 as uuidv4 } from 'uuid';
import { SignedXml } from 'xml-crypto';
import { ISecurity } from '../types';
import { IWSSecurityCertOptions, IXmlSignerOptions } from './WSSecurityCert';

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

function dateStringForSOAP(date: Date): string {
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(): string {
return dateStringForSOAP(new Date());
}

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

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

function generateId(): string {
return uuidv4().replace(/-/gm, '');
}

function resolvePlaceholderInReferences(references: any[], bodyXpath: string) {
for (const ref of references) {
if (ref.xpath === bodyXpathPlaceholder) {
ref.xpath = bodyXpath;
}
}
}

const oasisBaseUri = 'http://docs.oasis-open.org/wss/2004/01';
const bodyXpathPlaceholder = '[[bodyXpath]]';

export class WSSecurityCertWithToken implements ISecurity {
private publicP12PEM: string;
private signer: any;
private signerOptions: IXmlSignerOptions = {};
private x509Id: string;
private hasTimeStamp: boolean;
private signatureTransformations: string[];
private created: string;
private expires: string;
private additionalReferences: string[] = [];
private username: string;
private password: string;

constructor(props: { privateKey: Buffer, publicKey: string, keyPassword?: string, username: string, password: string, options?: IWSSecurityCertOptions }) {
this.publicP12PEM = props.publicKey.toString()
.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.replace(/(\r\n|\n|\r)/gm, '');
this.username = props.username;
this.password = props.password;

this.signer = new SignedXml();
const opts = props.options || {};
if (opts.signatureAlgorithm === 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256') {
this.signer.signatureAlgorithm = opts.signatureAlgorithm;
this.signer.addReference(
bodyXpathPlaceholder,
['http://www.w3.org/2001/10/xml-exc-c14n#'],
'http://www.w3.org/2001/04/xmlenc#sha256',
);
}

if (opts.additionalReferences && opts.additionalReferences.length > 0) {
this.additionalReferences = opts.additionalReferences;
}

if (opts.signerOptions) {
const { signerOptions } = props.options;
this.signerOptions = signerOptions;
if (!this.signerOptions.existingPrefixes) {
this.signerOptions.existingPrefixes = {};
}
if (this.signerOptions.existingPrefixes && !this.signerOptions.existingPrefixes.wsse) {
this.signerOptions.existingPrefixes.wsse = `${oasisBaseUri}/oasis-200401-wss-wssecurity-secext-1.0.xsd`;
}
} else {
this.signerOptions = { existingPrefixes: { wsse: `${oasisBaseUri}/oasis-200401-wss-wssecurity-secext-1.0.xsd` } };
}

this.signer.signingKey = {
key: props.privateKey,
passphrase: props.keyPassword,
};
this.x509Id = `x509-${generateId()}`;
this.hasTimeStamp = typeof opts.hasTimeStamp === 'undefined' ? true : !!opts.hasTimeStamp;
this.signatureTransformations = Array.isArray(opts.signatureTransformations) ? opts.signatureTransformations
: ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#'];

this.signer.keyInfoProvider = {};
this.signer.keyInfoProvider.getKeyInfo = (key) => {
return `<wsse:SecurityTokenReference>` +
`<wsse:Reference URI="#${this.x509Id}" ValueType="${oasisBaseUri}/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>` +
`</wsse:SecurityTokenReference>`;
};
}

public postProcess(xml, envelopeKey) {
this.created = generateCreated();
this.expires = generateExpires();

let timestampStr = '';
if (this.hasTimeStamp) {
timestampStr =
`<Timestamp xmlns="${oasisBaseUri}/oasis-200401-wss-wssecurity-utility-1.0.xsd" Id="_1">` +
`<Created>${this.created}</Created>` +
`<Expires>${this.expires}</Expires>` +
`</Timestamp>`;
}
let usernameToken = '';
if (this.username) {
usernameToken = `<wsse:UsernameToken wsu:Id="SecurityToken-${this.created}" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">` +
`<wsse:Username>${this.username}</wsse:Username> ` +
`<wsse:Password>${this.password}</wsse:Password> ` +
`</wsse:UsernameToken>`;
}
const secHeader =
`<wsse:Security xmlns:wsse="${oasisBaseUri}/oasis-200401-wss-wssecurity-secext-1.0.xsd" ` +
`xmlns:wsu="${oasisBaseUri}/oasis-200401-wss-wssecurity-utility-1.0.xsd" ` +
`${envelopeKey}:mustUnderstand="1">` +
`<wsse:BinarySecurityToken ` +
`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>` +
usernameToken +
timestampStr +
`</wsse:Security>`;

const xmlWithSec = insertStr(secHeader, xml, xml.indexOf(`</${envelopeKey}:Header>`));

const references = this.signatureTransformations;

const bodyXpath = `//*[name(.)='${envelopeKey}:Body']`;
resolvePlaceholderInReferences(this.signer.references, bodyXpath);

if (!(this.signer.references.filter((ref) => (ref.xpath === bodyXpath)).length > 0)) {
this.signer.addReference(bodyXpath, references);
}

for (const name of this.additionalReferences) {
const xpath = `//*[name(.)='${name}']`;
if (!(this.signer.references.filter((ref) => (ref.xpath === xpath)).length > 0)) {
this.signer.addReference(xpath, references);
}
}

const timestampXpath = `//*[name(.)='wsse:Security']/*[local-name(.)='Timestamp']`;
if (this.hasTimeStamp && !(this.signer.references.filter((ref) => (ref.xpath === timestampXpath)).length > 0)) {
this.signer.addReference(timestampXpath, references);
}

this.signer.computeSignature(xmlWithSec, this.signerOptions);

return insertStr(this.signer.getSignatureXml(), xmlWithSec, xmlWithSec.indexOf('</wsse:Security>'));
}
}
1 change: 1 addition & 0 deletions src/security/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from './ClientSSLSecurityPFX';
export * from './NTLMSecurity';
export * from './WSSecurity';
export * from './WSSecurityCert';
export * from './WSSecurityCertWithToken';
export * from './WSSecurityPlusCert';
2 changes: 1 addition & 1 deletion src/soap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const debug = debugBuilder('node-soap:soap');
export const security = _security;
export { Client } from './client';
export { HttpClient } from './http';
export { BasicAuthSecurity, BearerSecurity, ClientSSLSecurity, ClientSSLSecurityPFX, NTLMSecurity, WSSecurity, WSSecurityCert, WSSecurityPlusCert } from './security';
export { BasicAuthSecurity, BearerSecurity, ClientSSLSecurity, ClientSSLSecurityPFX, NTLMSecurity, WSSecurity, WSSecurityCert, WSSecurityPlusCert, WSSecurityCertWithToken } from './security';
export { Server } from './server';
export { passwordDigest } from './utils';
export * from './types';
Expand Down
2 changes: 2 additions & 0 deletions src/wsdl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,8 @@ export class WSDL {
types.addChild(schema);
root.addChild(types);
stack.push(schema);
} else if (name === 'html') {
throw new Error(`Root element of WSDL was <html>. This is likely an authentication issue.`);
} else {
throw new Error('Unexpected root element of WSDL or include');
}
Expand Down
4 changes: 4 additions & 0 deletions test/client-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,10 @@ xit('should add namespace to array of objects', function (done) {
}
done();
});
})
.catch(function (err) {
assert.equal(err.message, 'Root element of WSDL was <html>. This is likely an authentication issue.');
done();
});
});

Expand Down
Loading
Loading