mocksaml/utils/response.ts

215 lines
6.9 KiB
TypeScript
Raw Normal View History

2022-02-21 14:31:47 +00:00
import { User } from '../types';
import xmlbuilder from 'xmlbuilder';
2022-02-21 17:02:03 +00:00
import crypto from 'crypto';
2022-02-21 15:36:25 +00:00
import { SignedXml, FileKeyInfo } from 'xml-crypto';
2022-02-22 06:00:14 +00:00
import { pki, util, asn1 } from 'node-forge';
2022-02-21 14:31:47 +00:00
2022-02-21 15:36:25 +00:00
const createResponseXML = async (params: {
2022-02-22 06:14:12 +00:00
idpIdentityId: string;
audience: string;
acsUrl: string;
user: User;
2022-02-21 14:31:47 +00:00
}): Promise<string> => {
2022-02-22 06:14:12 +00:00
const { idpIdentityId, audience, acsUrl, user } = params;
2022-02-21 14:31:47 +00:00
const authDate = new Date();
const authTimestamp = authDate.toISOString();
authDate.setMinutes(authDate.getMinutes() - 5);
const notBefore = authDate.toISOString();
authDate.setMinutes(authDate.getMinutes() + 10);
const notAfter = authDate.toISOString();
2022-02-22 06:14:12 +00:00
const inResponseTo = '_1234';
2022-02-23 08:20:49 +00:00
// const responseId = crypto.randomBytes(10).toString('hex');
2022-02-21 14:31:47 +00:00
const attributeStatement = {
'@xmlns:xs': 'http://www.w3.org/2001/XMLSchema',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
2022-02-22 06:14:12 +00:00
'saml:Attribute': [
2022-02-21 14:31:47 +00:00
{
'@Name': 'id',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
2022-02-23 08:20:49 +00:00
'@xmlns:xs': 'http://www.w3.org/2001/XMLSchema',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'xs:string',
2022-02-21 14:31:47 +00:00
'#text': user.id,
2022-02-22 06:14:12 +00:00
},
2022-02-21 14:31:47 +00:00
},
{
'@Name': 'email',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
2022-02-23 08:20:49 +00:00
'@xmlns:xs': 'http://www.w3.org/2001/XMLSchema',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'xs:string',
2022-02-21 14:31:47 +00:00
'#text': user.email,
2022-02-22 06:14:12 +00:00
},
2022-02-21 14:31:47 +00:00
},
{
'@Name': 'firstName',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
2022-02-23 08:20:49 +00:00
'@xmlns:xs': 'http://www.w3.org/2001/XMLSchema',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'xs:string',
2022-02-21 14:31:47 +00:00
'#text': user.firstName,
2022-02-22 06:14:12 +00:00
},
2022-02-21 14:31:47 +00:00
},
{
'@Name': 'lastName',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
2022-02-23 08:20:49 +00:00
'@xmlns:xs': 'http://www.w3.org/2001/XMLSchema',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'xs:string',
2022-02-21 14:31:47 +00:00
'#text': user.lastName,
2022-02-22 06:14:12 +00:00
},
2022-02-21 14:31:47 +00:00
},
2022-02-22 06:14:12 +00:00
],
};
2022-02-21 14:31:47 +00:00
const nodes = {
2022-02-22 06:14:12 +00:00
'samlp:Response': {
2022-02-21 14:31:47 +00:00
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@Version': '2.0',
2022-02-23 08:20:49 +00:00
'@ID': crypto.randomBytes(10).toString('hex'),
2022-02-21 14:31:47 +00:00
'@Destination': acsUrl,
'@InResponseTo': inResponseTo,
'@IssueInstant': authTimestamp,
2022-02-23 08:20:49 +00:00
'saml:Issuer': {
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'@Format': 'urn:oasis:names:tc:SAML:2.0:assertion',
'#text': idpIdentityId,
},
2022-02-21 14:31:47 +00:00
'samlp:Status': {
'samlp:StatusCode': {
2022-02-22 06:14:12 +00:00
'@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success',
},
2022-02-21 14:31:47 +00:00
},
'saml:Assertion': {
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'@Version': '2.0',
2022-02-23 08:20:49 +00:00
'@ID': crypto.randomBytes(10).toString('hex'),
2022-02-21 14:31:47 +00:00
'@IssueInstant': authTimestamp,
'saml:Issuer': {
'#text': idpIdentityId,
},
'saml:Subject': {
'saml:NameID': {
'@Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
'#text': user.email,
2022-02-22 06:14:12 +00:00
},
2022-02-21 14:31:47 +00:00
},
'saml:Conditions': {
'@NotBefore': notBefore,
'@NotOnOrAfter': notAfter,
'saml:AudienceRestriction': {
'saml:Audience': {
'#text': audience,
2022-02-22 06:14:12 +00:00
},
},
2022-02-21 14:31:47 +00:00
},
'saml:AuthnStatement': {
'@AuthnInstant': authTimestamp,
'@SessionIndex': '_YIlFoNFzLMDYxdwf-T_BuimfkGa5qhKg',
'saml:AuthnContext': {
'saml:AuthnContextClassRef': {
2022-02-22 06:14:12 +00:00
'#text': 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified',
},
},
2022-02-21 14:31:47 +00:00
},
'saml:AttributeStatement': attributeStatement,
},
2022-02-22 06:14:12 +00:00
},
};
2022-02-21 14:31:47 +00:00
2022-02-23 08:20:49 +00:00
return xmlbuilder.create(nodes, { encoding: 'UTF-8' }).end();
2022-02-21 14:31:47 +00:00
};
// Create the HTML form to submit the response
const createResponseForm = (relayState: string, encodedSamlResponse: string, acsUrl: string) => {
const formElements = [
'<!DOCTYPE html>',
'<html>',
'<head>',
'<meta charset="utf-8">',
'<meta http-equiv="x-ua-compatible" content="ie=edge">',
'</head>',
'<body onload="document.forms[0].submit()">',
'<noscript>',
'<p>Note: Since your browser does not support JavaScript, you must press the Continue button once to proceed.</p>',
'</noscript>',
'<form method="post" action="' + encodeURI(acsUrl) + '">',
'<input type="hidden" name="RelayState" value="' + relayState + '"/>',
'<input type="hidden" name="SAMLResponse" value="' + encodedSamlResponse + '"/>',
'<input type="submit" value="Continue" />',
'</form>',
'<script>document.forms[0].style.display="none";</script>',
'</body>',
'</html>',
];
return formElements.join('');
};
2022-02-22 06:00:14 +00:00
function getPublicKeyPemFromCertificate(x509Certificate: string) {
const certDerBytes = util.decode64(x509Certificate);
const obj = asn1.fromDer(certDerBytes);
const cert = pki.certificateFromAsn1(obj);
return pki.publicKeyToPem(cert.publicKey);
}
const stripCertHeaderAndFooter = (cert: string): string => {
cert = cert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, '');
cert = cert.replace(/-+END CERTIFICATE-+\r?\n?/, '');
cert = cert.replace(/\r\n/g, '\n');
return cert;
};
function GetKeyInfo(x509Certificate: string, signatureConfig: any = {}) {
x509Certificate = stripCertHeaderAndFooter(x509Certificate);
this.getKeyInfo = () => {
const prefix = signatureConfig.prefix ? `${signatureConfig.prefix}:` : '';
return `<${prefix}X509Data><${prefix}X509Certificate>${x509Certificate}</${prefix}X509Certificate></${prefix}X509Data>`;
};
this.getKey = () => {
return getPublicKeyPemFromCertificate(x509Certificate).toString();
};
}
2022-02-21 14:31:47 +00:00
const signResponseXML = async (xml: string, signingKey: any, publicKey: any): Promise<string> => {
2022-02-21 15:36:25 +00:00
const sig = new SignedXml();
2022-02-22 06:14:12 +00:00
const responseXPath =
'/*[local-name(.)="Response" 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"]';
2022-02-21 14:31:47 +00:00
2022-02-21 15:36:25 +00:00
sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
2022-02-22 06:00:14 +00:00
// @ts-ignore
2022-02-23 08:20:49 +00:00
sig.keyInfoProvider = new GetKeyInfo(publicKey, {
2022-02-23 12:34:57 +00:00
prefix: '',
2022-02-23 08:20:49 +00:00
});
2022-02-21 15:36:25 +00:00
sig.signingKey = signingKey;
2022-02-21 14:31:47 +00:00
2022-02-22 06:14:12 +00:00
sig.addReference(
responseXPath,
['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#'],
'http://www.w3.org/2001/04/xmlenc#sha256'
);
2022-02-21 15:36:25 +00:00
sig.computeSignature(xml, {
location: { reference: responseXPath + issuerXPath, action: 'after' },
});
return sig.getSignedXml();
2022-02-22 06:14:12 +00:00
};
2022-02-21 14:31:47 +00:00
2022-02-22 06:14:12 +00:00
export { createResponseXML, createResponseForm, signResponseXML };