cleanup util
This commit is contained in:
parent
21fa8eb87a
commit
aa76970fe7
15
data/idp-public.key
Normal file
15
data/idp-public.key
Normal 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
88
package-lock.json
generated
@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"name": "fake",
|
||||
"dependencies": {
|
||||
"@types/xml-crypto": "^1.4.2",
|
||||
"axios": "^0.24.0",
|
||||
"next": "12.1.0",
|
||||
"node-fetch": "^3.2.0",
|
||||
@ -13,6 +14,7 @@
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"webpack-filter-warnings-plugin": "^1.2.1",
|
||||
"xml-crypto": "^2.1.3",
|
||||
"xml2js": "^0.4.23",
|
||||
"xmlbuilder": "^15.1.1"
|
||||
},
|
||||
@ -427,8 +429,7 @@
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
|
||||
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg=="
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
@ -459,6 +460,23 @@
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"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": {
|
||||
"version": "0.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
|
||||
@ -805,6 +823,14 @@
|
||||
"@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": {
|
||||
"version": "1.2.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
@ -7962,6 +8000,14 @@
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
@ -8242,8 +8288,7 @@
|
||||
"@types/node": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
|
||||
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg=="
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
@ -8274,6 +8319,22 @@
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"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": {
|
||||
"version": "0.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
|
||||
@ -8554,6 +8615,11 @@
|
||||
"@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": {
|
||||
"version": "1.2.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.4.23",
|
||||
"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",
|
||||
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="
|
||||
},
|
||||
"xpath": {
|
||||
"version": "0.0.32",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
|
||||
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/xml-crypto": "^1.4.2",
|
||||
"axios": "^0.24.0",
|
||||
"next": "12.1.0",
|
||||
"node-fetch": "^3.2.0",
|
||||
@ -15,6 +16,7 @@
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"webpack-filter-warnings-plugin": "^1.2.1",
|
||||
"xml-crypto": "^2.1.3",
|
||||
"xml2js": "^0.4.23",
|
||||
"xmlbuilder": "^15.1.1"
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createCertificate, createIdPMetadataXML } from '../../../../utils';
|
||||
import { fetchPublicKey, createIdPMetadataXML } from '../../../../utils';
|
||||
import { IdPMetadata } from '../../../../types';
|
||||
import stream from 'stream';
|
||||
import { promisify } from 'util';
|
||||
@ -24,7 +24,7 @@ export default async function handler(
|
||||
const xml = await createIdPMetadataXML({
|
||||
idpEntityId: config.entityId,
|
||||
idpSsoUrl: config.ssoUrl,
|
||||
certificate: await createCertificate(),
|
||||
certificate: await fetchPublicKey(),
|
||||
});
|
||||
|
||||
res.setHeader('Content-type', 'text/xml');
|
||||
|
||||
@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createResponseForm, createSAMLResponseXML } from 'utils';
|
||||
import { User } from 'types';
|
||||
import config from '../../../lib/env'
|
||||
import { signResponseXML } from 'utils/response';
|
||||
import { fetchPrivateKey, fetchPublicKey } from 'utils/certificate';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@ -37,9 +39,11 @@ export default async function handler(
|
||||
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);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { GetStaticProps } from "next";
|
||||
import { IdPMetadata } from "../types";
|
||||
import config from "../lib/env";
|
||||
import { createCertificate } from "../utils";
|
||||
import { fetchPublicKey } from "../utils";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Head from "next/head";
|
||||
@ -10,7 +10,7 @@ export const getStaticProps: GetStaticProps = async () => {
|
||||
const metadata: IdPMetadata = {
|
||||
ssoUrl: config.ssoUrl,
|
||||
entityId: config.entityId,
|
||||
certificate: await createCertificate(),
|
||||
certificate: await fetchPublicKey(),
|
||||
};
|
||||
|
||||
return {
|
||||
@ -32,7 +32,7 @@ const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
|
||||
<p className="font-extrabold">Entity ID</p>
|
||||
<p className="col-span-2">{metadata.entityId}</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}
|
||||
</p>
|
||||
<br></br>
|
||||
|
||||
23
utils/certificate.ts
Normal file
23
utils/certificate.ts
Normal 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,
|
||||
}
|
||||
222
utils/index.ts
222
utils/index.ts
@ -1,219 +1,3 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import xml2js from 'xml2js';
|
||||
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,
|
||||
};
|
||||
export { fetchPrivateKey, fetchPublicKey } from './certificate'
|
||||
export {extractSAMLRequestAttributes, createIdPMetadataXML} from './request'
|
||||
export { createSAMLResponseXML, createResponseForm, signResponseXML } from './response'
|
||||
57
utils/request.ts
Normal file
57
utils/request.ts
Normal 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
165
utils/response.ts
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user