Skip to content

Commit

Permalink
Move signXML function to xml module
Browse files Browse the repository at this point in the history
  • Loading branch information
forty committed Apr 5, 2021
1 parent 4af7dd0 commit ab4e003
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 60 deletions.
29 changes: 3 additions & 26 deletions src/node-saml/saml-post-signing.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,19 @@
import { SignedXml } from "xml-crypto";
import * as algorithms from "./algorithms";
import { SamlSigningOptions } from "./types";
import { signXml } from "./xml";

const authnRequestXPath =
'/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';
const issuerXPath =
'/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]';
const defaultTransforms = [
"http://www.w3.org/2000/09/xmldsig#enveloped-signature",
"http://www.w3.org/2001/10/xml-exc-c14n#",
];

export function signSamlPost(
samlMessage: string,
xpath: string,
options: SamlSigningOptions
): string {
if (!samlMessage) throw new Error("samlMessage is required");
if (!xpath) throw new Error("xpath is required");
if (!options) {
options = {} as SamlSigningOptions;
}

if (options.privateKey == null) throw new Error("options.privateKey is required");

const transforms = options.xmlSignatureTransforms || defaultTransforms;
const sig = new SignedXml();
if (options.signatureAlgorithm) {
sig.signatureAlgorithm = algorithms.getSigningAlgorithm(options.signatureAlgorithm);
}
sig.addReference(xpath, transforms, algorithms.getDigestAlgorithm(options.digestAlgorithm));
sig.signingKey = options.privateKey;
sig.computeSignature(samlMessage, {
location: { reference: xpath + issuerXPath, action: "after" },
});
return sig.getSignedXml();
return signXml(samlMessage, xpath, { reference: xpath + issuerXPath, action: "after" }, options);
}

export function signAuthnRequestPost(authnRequest: string, options: SamlSigningOptions) {
export function signAuthnRequestPost(authnRequest: string, options: SamlSigningOptions): string {
return signSamlPost(authnRequest, authnRequestXPath, options);
}
4 changes: 3 additions & 1 deletion src/node-saml/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as algorithms from "./algorithms";
import { signAuthnRequestPost } from "./saml-post-signing";
import { ParsedQs } from "qs";
import {
isValidSamlSigningOptions,
AudienceRestrictionXML,
AuthorizeRequestXML,
CertCallback,
Expand Down Expand Up @@ -355,7 +356,8 @@ class SAML {
}

let stringRequest = buildXmlBuilderObject(request, false);
if (isHttpPostBinding && this.options.privateKey != null) {
// TODO: maybe we should always sign here
if (isHttpPostBinding && isValidSamlSigningOptions(this.options)) {
stringRequest = signAuthnRequestPost(stringRequest, this.options);
}
return stringRequest;
Expand Down
10 changes: 8 additions & 2 deletions src/node-saml/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import type { CacheProvider } from "./inmemory-cache-provider";
export type SignatureAlgorithm = "sha1" | "sha256" | "sha512";

export interface SamlSigningOptions {
privateKey?: string | Buffer;
privateKey: string | Buffer;
signatureAlgorithm?: SignatureAlgorithm;
xmlSignatureTransforms?: string[];
digestAlgorithm?: string;
}

export const isValidSamlSigningOptions = (
options: Partial<SamlSigningOptions>
): options is SamlSigningOptions => {
return options.privateKey != null;
};

export interface AudienceRestrictionXML {
Audience?: XMLObject[];
}
Expand Down Expand Up @@ -75,7 +81,7 @@ interface SamlScopingConfig {
* The options required to use a SAML strategy
* These may be provided by means of defaults specified in the constructor
*/
export interface SamlOptions extends SamlSigningOptions, MandatorySamlOptions {
export interface SamlOptions extends Partial<SamlSigningOptions>, MandatorySamlOptions {
// Core
callbackUrl?: string;
path: string;
Expand Down
38 changes: 7 additions & 31 deletions src/node-saml/utility.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SignedXml } from "xml-crypto";
import { SamlSigningOptions } from "./types";
import * as algorithms from "./algorithms";
import { signXml } from "./xml";

export function assertRequired<T>(value: T | null | undefined, error?: string): T {
if (value === undefined || value === null || (typeof value === "string" && value.length === 0)) {
Expand All @@ -10,37 +9,14 @@ export function assertRequired<T>(value: T | null | undefined, error?: string):
}
}

export function signXml(samlMessage: string, xpath: string, options: SamlSigningOptions): string {
const defaultTransforms = [
"http://www.w3.org/2000/09/xmldsig#enveloped-signature",
"http://www.w3.org/2001/10/xml-exc-c14n#",
];

if (!samlMessage) throw new Error("samlMessage is required");
if (!xpath) throw new Error("xpath is required");
if (!options) {
options = {} as SamlSigningOptions;
}

if (!options.privateKey) throw new Error("options.privateKey is required");

const transforms = options.xmlSignatureTransforms || defaultTransforms;
const sig = new SignedXml();
if (options.signatureAlgorithm) {
sig.signatureAlgorithm = algorithms.getSigningAlgorithm(options.signatureAlgorithm);
}
sig.addReference(xpath, transforms, algorithms.getDigestAlgorithm(options.digestAlgorithm));
sig.signingKey = options.privateKey;
sig.computeSignature(samlMessage, {
location: { reference: xpath, action: "append" },
});

return sig.getSignedXml();
}

export function signXmlResponse(samlMessage: string, options: SamlSigningOptions): string {
const responseXpath =
'//*[local-name(.)="Response" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';

return signXml(samlMessage, responseXpath, options);
return signXml(
samlMessage,
responseXpath,
{ reference: responseXpath, action: "append" },
options
);
}
37 changes: 37 additions & 0 deletions src/node-saml/xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as xmlenc from "xml-encryption";
import * as xmldom from "xmldom";
import * as xml2js from "xml2js";
import * as xmlbuilder from "xmlbuilder";
import { isValidSamlSigningOptions, SamlSigningOptions } from "./types";
import * as algorithms from "./algorithms";

type SelectedValue = string | number | boolean | Node;

Expand Down Expand Up @@ -93,6 +95,41 @@ export const validateXmlSignatureForCert = (
return sig.checkSignature(fullXml);
};

interface XmlSignatureLocation {
reference: string;
action: "append" | "prepend" | "before" | "after";
}

export const signXml = (
xml: string,
xpath: string,
location: XmlSignatureLocation,
options: SamlSigningOptions
): string => {
const defaultTransforms = [
"http://www.w3.org/2000/09/xmldsig#enveloped-signature",
"http://www.w3.org/2001/10/xml-exc-c14n#",
];

if (!xml) throw new Error("samlMessage is required");
if (!location) throw new Error("location is required");
if (!options) throw new Error("options is required");
if (!isValidSamlSigningOptions(options)) throw new Error("options.privateKey is required");

const transforms = options.xmlSignatureTransforms ?? defaultTransforms;
const sig = new xmlCrypto.SignedXml();
if (options.signatureAlgorithm != null) {
sig.signatureAlgorithm = algorithms.getSigningAlgorithm(options.signatureAlgorithm);
}
sig.addReference(xpath, transforms, algorithms.getDigestAlgorithm(options.digestAlgorithm));
sig.signingKey = options.privateKey;
sig.computeSignature(xml, {
location,
});

return sig.getSignedXml();
};

export const parseDomFromString = (xml: string): Document => {
return new xmldom.DOMParser().parseFromString(xml);
};
Expand Down

0 comments on commit ab4e003

Please sign in to comment.