import { User } from '../types'; import xmlbuilder from 'xmlbuilder'; import crypto from 'crypto'; import { SignedXml, FileKeyInfo } from 'xml-crypto'; import { pki, util, asn1 } from 'node-forge'; const createResponseXML = async (params: { idpIdentityId: string; audience: string; acsUrl: string; samlReqId: string; user: User; }): Promise => { const { idpIdentityId, audience, acsUrl, user, samlReqId } = params; 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(); const inResponseTo = samlReqId; // const responseId = crypto.randomBytes(10).toString('hex'); const attributeStatement = { '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'saml:Attribute': [ { '@Name': 'id', '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', '@xsi:type': 'xs:string', '#text': user.id, }, }, { '@Name': 'email', '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', '@xsi:type': 'xs:string', '#text': user.email, }, }, { '@Name': 'firstName', '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', '@xsi:type': 'xs:string', '#text': user.firstName, }, }, { '@Name': 'lastName', '@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'saml:AttributeValue': { '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', '@xsi:type': 'xs:string', '#text': user.lastName, }, }, ], }; const nodes = { 'samlp:Response': { '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', '@Version': '2.0', '@ID': crypto.randomBytes(10).toString('hex'), '@Destination': acsUrl, '@InResponseTo': inResponseTo, '@IssueInstant': authTimestamp, 'saml:Issuer': { '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', '@Format': 'urn:oasis:names:tc:SAML:2.0:assertion', '#text': idpIdentityId, }, 'samlp:Status': { 'samlp:StatusCode': { '@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success', }, }, 'saml:Assertion': { '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', '@Version': '2.0', '@ID': crypto.randomBytes(10).toString('hex'), '@IssueInstant': authTimestamp, 'saml:Issuer': { '#text': idpIdentityId, }, 'saml:Subject': { 'saml:NameID': { '@Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', '#text': user.email, }, }, 'saml:Conditions': { '@NotBefore': notBefore, '@NotOnOrAfter': notAfter, 'saml:AudienceRestriction': { 'saml:Audience': { '#text': audience, }, }, }, 'saml:AuthnStatement': { '@AuthnInstant': authTimestamp, '@SessionIndex': '_YIlFoNFzLMDYxdwf-T_BuimfkGa5qhKg', 'saml:AuthnContext': { 'saml:AuthnContextClassRef': { '#text': 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified', }, }, }, 'saml:AttributeStatement': attributeStatement, }, }, }; return xmlbuilder.create(nodes, { encoding: 'UTF-8' }).end(); }; // Create the HTML form to submit the response const createResponseForm = (relayState: string, encodedSamlResponse: string, acsUrl: string) => { const formElements = [ '', '', '', '', '', '', '', '', '
', '', '', '', '
', '', '', '', ]; return formElements.join(''); }; 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}`; }; this.getKey = () => { return getPublicKeyPemFromCertificate(x509Certificate).toString(); }; } const signResponseXML = async (xml: string, signingKey: any, publicKey: any): Promise => { const sig = new SignedXml(); 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"]'; sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; // @ts-ignore sig.keyInfoProvider = new GetKeyInfo(publicKey, { prefix: '', }); sig.signingKey = signingKey; 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' ); sig.computeSignature(xml, { location: { reference: responseXPath + issuerXPath, action: 'after' }, }); return sig.getSignedXml(); }; export { createResponseXML, createResponseForm, signResponseXML };