diff --git a/data/idp-private-key.txt b/data/idp-private.key
similarity index 100%
rename from data/idp-private-key.txt
rename to data/idp-private.key
diff --git a/data/idp-public-key.txt b/data/idp-public-key.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/data/idp-public.key b/data/idp-public.key
new file mode 100644
index 0000000..74f2978
--- /dev/null
+++ b/data/idp-public.key
@@ -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-----
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 6a09597..07b94c5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 06f6b49..bed0a4f 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/pages/api/saml/metadata/download.ts b/pages/api/saml/metadata/download.ts
index 3fe59a9..beee0f3 100644
--- a/pages/api/saml/metadata/download.ts
+++ b/pages/api/saml/metadata/download.ts
@@ -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');
diff --git a/pages/api/saml/sso.ts b/pages/api/saml/sso.ts
index eace963..4d151a4 100644
--- a/pages/api/saml/sso.ts
+++ b/pages/api/saml/sso.ts
@@ -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);
diff --git a/pages/index.tsx b/pages/index.tsx
index 201b8fb..7c3431c 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -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 }) => {
Entity ID
{metadata.entityId}
Certificate
-
+
{metadata.certificate}
diff --git a/utils/certificate.ts b/utils/certificate.ts
new file mode 100644
index 0000000..ea9565a
--- /dev/null
+++ b/utils/certificate.ts
@@ -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,
+}
\ No newline at end of file
diff --git a/utils/index.ts b/utils/index.ts
index 4d891f3..ffbf4df 100644
--- a/utils/index.ts
+++ b/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> => {
- 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 => {
- 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 => {
- 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 = [
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- ];
-
- return formElements.join('');
-};
-
-export {
- parseXML,
- extractSAMLRequestAttributes,
- createIdPMetadataXML,
- createSAMLResponseXML,
- createCertificate,
- extractCert,
-};
\ No newline at end of file
+export { fetchPrivateKey, fetchPublicKey } from './certificate'
+export {extractSAMLRequestAttributes, createIdPMetadataXML} from './request'
+export { createSAMLResponseXML, createResponseForm, signResponseXML } from './response'
\ No newline at end of file
diff --git a/utils/request.ts b/utils/request.ts
new file mode 100644
index 0000000..b40485c
--- /dev/null
+++ b/utils/request.ts
@@ -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> => {
+ 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 => {
+ 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,
+}
\ No newline at end of file
diff --git a/utils/response.ts b/utils/response.ts
new file mode 100644
index 0000000..25f68c1
--- /dev/null
+++ b/utils/response.ts
@@ -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 => {
+ 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 = [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ];
+
+ return formElements.join('');
+};
+
+const signResponseXML = async (xml: string, signingKey: any, publicKey: any): Promise => {
+ 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
+}
\ No newline at end of file