cleanup util

This commit is contained in:
Kiran 2022-02-21 20:01:47 +05:30
parent 21fa8eb87a
commit aa76970fe7
12 changed files with 360 additions and 230 deletions

15
data/idp-public.key Normal file
View File

@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICSjCCAbOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBCMQswCQYDVQQGEwJ1czEN
MAsGA1UECAwERGVtbzEPMA0GA1UECgwGQm94eUhRMRMwEQYDVQQDDApib3h5aHEu
Y29tMB4XDTIyMDExMzE3NTQ1NVoXDTIzMDExMzE3NTQ1NVowQjELMAkGA1UEBhMC
dXMxDTALBgNVBAgMBERlbW8xDzANBgNVBAoMBkJveHlIUTETMBEGA1UEAwwKYm94
eWhxLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA4lbyAVpXmp1bGPGn
PfauUzTvPil0gDJaGBTYQ50A7lDLrD0rh/SbsRY5e8VA2JnYaKT7k53FL4n9ogjx
HQRT4b7s9ZjUUC7BHYPd4CzATjf6Iy48bbout2VphuZdWjwbY1uEfolaZR2QU4IR
4RYfa4L4fGZufA8ayunCWXTackMCAwEAAaNQME4wHQYDVR0OBBYEFKk0NXw5l0fq
MQ3GW4mNzazrZeEQMB8GA1UdIwQYMBaAFKk0NXw5l0fqMQ3GW4mNzazrZeEQMAwG
A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQENBQADgYEABIL+uv5KbnqLnvbeyglcuDSf
MVlPqMlvvliPLZa2TGluutL3t+jFfJNi6Vavd4BNyVsCYRe/ab8+/nok1Lu/IqKF
vifu1QGHsF1vKyafmVC8cMX/lxsvjedsOs++59yOAHAgXn+0IuBwupinKF4Tuqd7
n5gl9V4czyfFtrJUCQc=
-----END CERTIFICATE-----

88
package-lock.json generated
View File

@ -6,6 +6,7 @@
"": { "": {
"name": "fake", "name": "fake",
"dependencies": { "dependencies": {
"@types/xml-crypto": "^1.4.2",
"axios": "^0.24.0", "axios": "^0.24.0",
"next": "12.1.0", "next": "12.1.0",
"node-fetch": "^3.2.0", "node-fetch": "^3.2.0",
@ -13,6 +14,7 @@
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"webpack-filter-warnings-plugin": "^1.2.1", "webpack-filter-warnings-plugin": "^1.2.1",
"xml-crypto": "^2.1.3",
"xml2js": "^0.4.23", "xml2js": "^0.4.23",
"xmlbuilder": "^15.1.1" "xmlbuilder": "^15.1.1"
}, },
@ -427,8 +429,7 @@
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "17.0.8", "version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg=="
"dev": true
}, },
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
@ -459,6 +460,23 @@
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true "dev": true
}, },
"node_modules/@types/xml-crypto": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.2.tgz",
"integrity": "sha512-1kT+3gVkeBDg7Ih8NefxGYfCApwZViMIs5IEs5AXF6Fpsrnf9CLAEIRh0DYb1mIcRcvysVbe27cHsJD6rJi36w==",
"dependencies": {
"@types/node": "*",
"xpath": "0.0.27"
}
},
"node_modules/@types/xml-crypto/node_modules/xpath": {
"version": "0.0.27",
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",
"integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==",
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/@types/xml2js": { "node_modules/@types/xml2js": {
"version": "0.4.9", "version": "0.4.9",
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
@ -805,6 +823,14 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"node_modules/@xmldom/xmldom": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
"integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@xtuc/ieee754": { "node_modules/@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -7934,6 +7960,18 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}, },
"node_modules/xml-crypto": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-2.1.3.tgz",
"integrity": "sha512-MpXZwnn9JK0mNPZ5mnFIbNnQa+8lMGK4NtnX2FlJMfMWR60sJdFO9X72yO6ji068pxixzk53O7x0/iSKh6IhyQ==",
"dependencies": {
"@xmldom/xmldom": "^0.7.0",
"xpath": "0.0.32"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xml2js": { "node_modules/xml2js": {
"version": "0.4.23", "version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
@ -7962,6 +8000,14 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/xpath": {
"version": "0.0.32",
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==",
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -8242,8 +8288,7 @@
"@types/node": { "@types/node": {
"version": "17.0.8", "version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg=="
"dev": true
}, },
"@types/parse-json": { "@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
@ -8274,6 +8319,22 @@
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true "dev": true
}, },
"@types/xml-crypto": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.2.tgz",
"integrity": "sha512-1kT+3gVkeBDg7Ih8NefxGYfCApwZViMIs5IEs5AXF6Fpsrnf9CLAEIRh0DYb1mIcRcvysVbe27cHsJD6rJi36w==",
"requires": {
"@types/node": "*",
"xpath": "0.0.27"
},
"dependencies": {
"xpath": {
"version": "0.0.27",
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",
"integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ=="
}
}
},
"@types/xml2js": { "@types/xml2js": {
"version": "0.4.9", "version": "0.4.9",
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
@ -8554,6 +8615,11 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"@xmldom/xmldom": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
"integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A=="
},
"@xtuc/ieee754": { "@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -14219,6 +14285,15 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}, },
"xml-crypto": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-2.1.3.tgz",
"integrity": "sha512-MpXZwnn9JK0mNPZ5mnFIbNnQa+8lMGK4NtnX2FlJMfMWR60sJdFO9X72yO6ji068pxixzk53O7x0/iSKh6IhyQ==",
"requires": {
"@xmldom/xmldom": "^0.7.0",
"xpath": "0.0.32"
}
},
"xml2js": { "xml2js": {
"version": "0.4.23", "version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
@ -14240,6 +14315,11 @@
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==" "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="
}, },
"xpath": {
"version": "0.0.32",
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -8,6 +8,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@types/xml-crypto": "^1.4.2",
"axios": "^0.24.0", "axios": "^0.24.0",
"next": "12.1.0", "next": "12.1.0",
"node-fetch": "^3.2.0", "node-fetch": "^3.2.0",
@ -15,6 +16,7 @@
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"webpack-filter-warnings-plugin": "^1.2.1", "webpack-filter-warnings-plugin": "^1.2.1",
"xml-crypto": "^2.1.3",
"xml2js": "^0.4.23", "xml2js": "^0.4.23",
"xmlbuilder": "^15.1.1" "xmlbuilder": "^15.1.1"
}, },

View File

@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { createCertificate, createIdPMetadataXML } from '../../../../utils'; import { fetchPublicKey, createIdPMetadataXML } from '../../../../utils';
import { IdPMetadata } from '../../../../types'; import { IdPMetadata } from '../../../../types';
import stream from 'stream'; import stream from 'stream';
import { promisify } from 'util'; import { promisify } from 'util';
@ -24,7 +24,7 @@ export default async function handler(
const xml = await createIdPMetadataXML({ const xml = await createIdPMetadataXML({
idpEntityId: config.entityId, idpEntityId: config.entityId,
idpSsoUrl: config.ssoUrl, idpSsoUrl: config.ssoUrl,
certificate: await createCertificate(), certificate: await fetchPublicKey(),
}); });
res.setHeader('Content-type', 'text/xml'); res.setHeader('Content-type', 'text/xml');

View File

@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { createResponseForm, createSAMLResponseXML } from 'utils'; import { createResponseForm, createSAMLResponseXML } from 'utils';
import { User } from 'types'; import { User } from 'types';
import config from '../../../lib/env' import config from '../../../lib/env'
import { signResponseXML } from 'utils/response';
import { fetchPrivateKey, fetchPublicKey } from 'utils/certificate';
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -37,9 +39,11 @@ export default async function handler(
user: user, user: user,
}); });
console.log(xml) const signingKey = await fetchPrivateKey();
const publicKey = await fetchPublicKey();
const xmlSigned = await signResponseXML(xml, signingKey, publicKey);
const encodedSamlResponse = Buffer.from(xml).toString('base64'); const encodedSamlResponse = Buffer.from(xmlSigned).toString('base64');
const html = createResponseForm(relayState, encodedSamlResponse, acsUrl); const html = createResponseForm(relayState, encodedSamlResponse, acsUrl);

View File

@ -1,7 +1,7 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { IdPMetadata } from "../types"; import { IdPMetadata } from "../types";
import config from "../lib/env"; import config from "../lib/env";
import { createCertificate } from "../utils"; import { fetchPublicKey } from "../utils";
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import Head from "next/head"; import Head from "next/head";
@ -10,7 +10,7 @@ export const getStaticProps: GetStaticProps = async () => {
const metadata: IdPMetadata = { const metadata: IdPMetadata = {
ssoUrl: config.ssoUrl, ssoUrl: config.ssoUrl,
entityId: config.entityId, entityId: config.entityId,
certificate: await createCertificate(), certificate: await fetchPublicKey(),
}; };
return { return {
@ -32,7 +32,7 @@ const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
<p className="font-extrabold">Entity ID</p> <p className="font-extrabold">Entity ID</p>
<p className="col-span-2">{metadata.entityId}</p> <p className="col-span-2">{metadata.entityId}</p>
<p className="font-extrabold">Certificate</p> <p className="font-extrabold">Certificate</p>
<p className="min-w-0 overflow-auto text-sm col-span-2"> <p className="min-w-0 col-span-2 overflow-auto text-sm">
{metadata.certificate} {metadata.certificate}
</p> </p>
<br></br> <br></br>

23
utils/certificate.ts Normal file
View File

@ -0,0 +1,23 @@
import { promises as fs } from 'fs';
import path from 'path';
const fetchPublicKey = async () => {
return await fs.readFile(path.join('data', 'idp-public.key'), 'utf8');
};
const fetchPrivateKey = async () => {
return await fs.readFile(path.join('data', 'idp-private.key'), 'utf8');
}
const extractCert = (certificate: string) => {
return certificate
.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.trim();
};
export {
fetchPublicKey,
fetchPrivateKey,
extractCert,
}

View File

@ -1,219 +1,3 @@
import { promises as fs } from 'fs'; export { fetchPrivateKey, fetchPublicKey } from './certificate'
import path from 'path'; export {extractSAMLRequestAttributes, createIdPMetadataXML} from './request'
import xml2js from 'xml2js'; export { createSAMLResponseXML, createResponseForm, signResponseXML } from './response'
import { User } from '../types';
import {promisify} from 'util';
import zlib from 'zlib';
import xmlbuilder from 'xmlbuilder';
import crypto from 'crypto';
const inflateRawSync = promisify(zlib.inflateRawSync)
// Parse XML
const parseXML = (xml: string): Promise<Record<string, any>> => {
return new Promise((resolve, reject) => {
xml2js.parseString(xml, (err: Error, result: any) => {
if(err) {
reject(err);
}
resolve(result);
});
});
};
// Parse SAMLRequest attributes
const extractSAMLRequestAttributes = async (samlRequest: string) => {
// const request = await inflateRawSync(Buffer.from(samlRequest, 'base64')).toString();
// const result = await parseXML(request);
// const attributes = result['samlp:AuthnRequest']['$'];
return {
id: '123',
acsUrl: 'https://hookb.in/NOrYqkDLnXse8mNNlDXx',
providerName: 'BoxyHQ',
};
};
const createIdPMetadataXML = async ({
idpEntityId,
idpSsoUrl,
certificate,
}: {
idpEntityId: string;
idpSsoUrl: string;
certificate: string;
}): Promise<string> => {
const xmlPath = path.join('data', 'idp-metadata.xml');
const xml = await fs.readFile(xmlPath, 'utf8');
return xml
.replace('idp_entity_id', idpEntityId)
.replace('idp_certificate', extractCert(certificate))
.replace(/idp_sso_url/g, idpSsoUrl);
};
const createCertificate = async () => {
const certificateFilePath = path.join('data', 'idp-public-key.txt');
return await fs.readFile(certificateFilePath, 'utf8');
};
const extractCert = (certificate: string) => {
return certificate
.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.trim();
};
const createSAMLResponseXML = async (params: {
idpIdentityId: string,
audience: string,
acsUrl: string,
user: User
}): Promise<string> => {
const {idpIdentityId, audience, acsUrl, user} = 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 = '_1234'
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': {
'#text': user.id,
}
},
{
'@Name': 'email',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
'#text': user.email,
}
},
{
'@Name': 'firstName',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
'#text': user.firstName,
}
},
{
'@Name': 'lastName',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
'#text': user.lastName,
}
},
]
}
const nodes = {
'samlp:Response':{
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@Version': '2.0',
'@ID': responseId,
'@Destination': acsUrl,
'@InResponseTo': inResponseTo,
'@IssueInstant': authTimestamp,
'saml:Issuer': {
'@xmlns:saml': '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': responseId,
'@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).end({ pretty: true});
};
const signResponseXML = (xml: string, signingKey: any, publicKey: any): string => {
return xml;
}
// Create the HTML form to submit the response
export 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('');
};
export {
parseXML,
extractSAMLRequestAttributes,
createIdPMetadataXML,
createSAMLResponseXML,
createCertificate,
extractCert,
};

57
utils/request.ts Normal file
View File

@ -0,0 +1,57 @@
import { promises as fs } from 'fs';
import path from 'path';
import xml2js from 'xml2js';
import {promisify} from 'util';
import zlib from 'zlib';
const inflateRawSync = promisify(zlib.inflateRawSync)
// Parse XML
const parseXML = (xml: string): Promise<Record<string, any>> => {
return new Promise((resolve, reject) => {
xml2js.parseString(xml, (err: Error, result: any) => {
if(err) {
reject(err);
}
resolve(result);
});
});
};
// Parse SAMLRequest attributes
const extractSAMLRequestAttributes = async (samlRequest: string) => {
// const request = await inflateRawSync(Buffer.from(samlRequest, 'base64')).toString();
// const result = await parseXML(request);
// const attributes = result['samlp:AuthnRequest']['$'];
return {
id: '123',
acsUrl: 'https://hookb.in/NOrYqkDLnXse8mNNlDXx',
providerName: 'BoxyHQ',
};
};
const createIdPMetadataXML = async ({
idpEntityId,
idpSsoUrl,
certificate,
}: {
idpEntityId: string;
idpSsoUrl: string;
certificate: string;
}): Promise<string> => {
const xmlPath = path.join('data', 'idp-metadata.xml');
const xml = await fs.readFile(xmlPath, 'utf8');
return xml
.replace('idp_entity_id', idpEntityId)
.replace('idp_certificate', certificate)
.replace(/idp_sso_url/g, idpSsoUrl);
};
export {
extractSAMLRequestAttributes,
createIdPMetadataXML,
}

165
utils/response.ts Normal file
View File

@ -0,0 +1,165 @@
import { User } from '../types';
import xmlbuilder from 'xmlbuilder';
import crypto from 'crypto';
import {SignedXml, FileKeyInfo} from 'xml-crypto';
const createSAMLResponseXML = async (params: {
idpIdentityId: string,
audience: string,
acsUrl: string,
user: User
}): Promise<string> => {
const {idpIdentityId, audience, acsUrl, user} = 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 = '_1234'
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': {
'#text': user.id,
}
},
{
'@Name': 'email',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
'#text': user.email,
}
},
{
'@Name': 'firstName',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
'#text': user.firstName,
}
},
{
'@Name': 'lastName',
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
'saml:AttributeValue': {
'#text': user.lastName,
}
},
]
}
const nodes = {
'samlp:Response':{
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@Version': '2.0',
'@ID': responseId,
'@Destination': acsUrl,
'@InResponseTo': inResponseTo,
'@IssueInstant': authTimestamp,
'saml:Issuer': {
'@xmlns:saml': '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': responseId,
'@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).end({ pretty: true});
};
// 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('');
};
const signResponseXML = async (xml: string, signingKey: any, publicKey: any): Promise<string> => {
return '';
// const sig = new SignedXml();
// sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
// sig.keyInfoProvider = new PubKeyInfo(publicKey);
// sig.signingKey = signingKey;
// sig.addReference(authnXPath, ['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: authnXPath + issuerXPath, action: 'after' },
// });
// return sig.getSignedXml();
}
export {
createSAMLResponseXML,
createResponseForm,
signResponseXML
}