From 141a920a50ad0b204c06bc1cbbbd52d3768ab6f2 Mon Sep 17 00:00:00 2001 From: Kiran Date: Sat, 15 Jan 2022 01:25:28 +0530 Subject: [PATCH] wip --- README.md | 2 +- data/idp-metadata.xml | 15 +++++++ data/saml-response.xml | 58 ++++++++++++++++++++++++++ pages/api/apps/metadata.ts | 17 ++++---- pages/api/apps/saml.ts | 42 ++++++++----------- pages/apps/saml.tsx | 42 +++++++++++++++++++ services/metadata.ts | 48 ---------------------- types/index.ts | 7 ++++ utils/index.ts | 84 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 235 insertions(+), 80 deletions(-) create mode 100644 data/idp-metadata.xml create mode 100644 data/saml-response.xml create mode 100644 pages/apps/saml.tsx create mode 100644 utils/index.ts diff --git a/README.md b/README.md index a32e3ed..08a4ccc 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,4 @@ - Fix the SAML metadata URL - Improve the UI -http://localhost:4000/api/apps/saml?RelayState=boxyhq_jackson_2fd72712996df6104811ff8cc233d1e2&SAMLRequest=PD94bWwgdmVyc2lvbj0iMS4wIj8%2BPHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iXzZiMmU1ZDdhMDRiNjEzMzAyZDhmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAyMi0wMS0xNFQxNToxODoxNi4yNTlaIiBQcm90b2NvbEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVBPU1QiIEFzc2VydGlvbkNvbnN1bWVyU2VydmljZVVSTD0iaHR0cHM6Ly8yOGEyLTEwMy0xNTMtMTA0LTQzLm5ncm9rLmlvL3Nzby9hY3MiIFByb3ZpZGVyTmFtZT0iQm94eUhRIj48c2FtbDpJc3N1ZXIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BaHR0cHM6Ly9zYW1sLmJveHlocS5jb208L3NhbWw6SXNzdWVyPjxTaWduYXR1cmUgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxTaWduZWRJbmZvPjxDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8%2BPFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48UmVmZXJlbmNlIFVSST0iI182YjJlNWQ3YTA0YjYxMzMwMmQ4ZiI%2BPFRyYW5zZm9ybXM%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8%2BPERpZ2VzdFZhbHVlPlBXVFlGek9hV1FvZHNqK21haEdnam1VQkZsVFk5cGlQNy8yVVkvNEZkN289PC9EaWdlc3RWYWx1ZT48L1JlZmVyZW5jZT48L1NpZ25lZEluZm8%2BPFNpZ25hdHVyZVZhbHVlPm5HbEMrZVkwMC82eDZQNlBMcGFoOVR5QzlRTnNKR0U3V0Y1czQ1SXZRbWlJWmwrSmhNVGRQeExBSjJ1dVpQMWZzZ1NwUllPZG5HanRLNjlobS9LOHZDVGlab29EODdjdkJmdXp4NVVNQ3NiV0VQSERJV01rV1k3S2ZrYk5ySjNMdVZyYTR0SEZvY2luQnVwRzNMeVQ5dUtwRzI1NlQrNm9LUDNOdkJSTzRROUFpWlk4czlxaVhzRE9QQlJRd3NEaEdNdGRJcGVDaGZQU2RQdFV1RXV3UEdRS1pZellFY0d3WHpOTGYydm5PaS9QUE5rRHA4QTdPN2FJd09HWFNDVVlqL29oNUo4RWRQdVpyRzFuL0ZMdm04L0dNY2pNTTYwQWZ1NFg3WGMzRlFRUG5aZ2lxdFlsd2YwbjFDemlkbnYvbDE3NjF6T3grWXJqb08zaUp0SHB6Zz09PC9TaWduYXR1cmVWYWx1ZT48L1NpZ25hdHVyZT48c2FtbHA6TmFtZUlEUG9saWN5IHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIiBBbGxvd0NyZWF0ZT0idHJ1ZSIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3D +http://localhost:4000/apps/saml?RelayState=boxyhq_jackson_2fd72712996df6104811ff8cc233d1e2&SAMLRequest=PD94bWwgdmVyc2lvbj0iMS4wIj8%2BPHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iXzZiMmU1ZDdhMDRiNjEzMzAyZDhmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAyMi0wMS0xNFQxNToxODoxNi4yNTlaIiBQcm90b2NvbEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVBPU1QiIEFzc2VydGlvbkNvbnN1bWVyU2VydmljZVVSTD0iaHR0cHM6Ly8yOGEyLTEwMy0xNTMtMTA0LTQzLm5ncm9rLmlvL3Nzby9hY3MiIFByb3ZpZGVyTmFtZT0iQm94eUhRIj48c2FtbDpJc3N1ZXIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BaHR0cHM6Ly9zYW1sLmJveHlocS5jb208L3NhbWw6SXNzdWVyPjxTaWduYXR1cmUgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxTaWduZWRJbmZvPjxDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8%2BPFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48UmVmZXJlbmNlIFVSST0iI182YjJlNWQ3YTA0YjYxMzMwMmQ4ZiI%2BPFRyYW5zZm9ybXM%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8%2BPERpZ2VzdFZhbHVlPlBXVFlGek9hV1FvZHNqK21haEdnam1VQkZsVFk5cGlQNy8yVVkvNEZkN289PC9EaWdlc3RWYWx1ZT48L1JlZmVyZW5jZT48L1NpZ25lZEluZm8%2BPFNpZ25hdHVyZVZhbHVlPm5HbEMrZVkwMC82eDZQNlBMcGFoOVR5QzlRTnNKR0U3V0Y1czQ1SXZRbWlJWmwrSmhNVGRQeExBSjJ1dVpQMWZzZ1NwUllPZG5HanRLNjlobS9LOHZDVGlab29EODdjdkJmdXp4NVVNQ3NiV0VQSERJV01rV1k3S2ZrYk5ySjNMdVZyYTR0SEZvY2luQnVwRzNMeVQ5dUtwRzI1NlQrNm9LUDNOdkJSTzRROUFpWlk4czlxaVhzRE9QQlJRd3NEaEdNdGRJcGVDaGZQU2RQdFV1RXV3UEdRS1pZellFY0d3WHpOTGYydm5PaS9QUE5rRHA4QTdPN2FJd09HWFNDVVlqL29oNUo4RWRQdVpyRzFuL0ZMdm04L0dNY2pNTTYwQWZ1NFg3WGMzRlFRUG5aZ2lxdFlsd2YwbjFDemlkbnYvbDE3NjF6T3grWXJqb08zaUp0SHB6Zz09PC9TaWduYXR1cmVWYWx1ZT48L1NpZ25hdHVyZT48c2FtbHA6TmFtZUlEUG9saWN5IHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIiBBbGxvd0NyZWF0ZT0idHJ1ZSIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3D diff --git a/data/idp-metadata.xml b/data/idp-metadata.xml new file mode 100644 index 0000000..9b18bba --- /dev/null +++ b/data/idp-metadata.xml @@ -0,0 +1,15 @@ + + + + + + + idp_certificate + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + \ No newline at end of file diff --git a/data/saml-response.xml b/data/saml-response.xml new file mode 100644 index 0000000..391b2b0 --- /dev/null +++ b/data/saml-response.xml @@ -0,0 +1,58 @@ + + + idp_entity_id + + + + + + + + + + + xPMc7SSHhFSWGljyk1L8mRE1M6otu0qlukR42E6QdiQ= + + + VXlnv1pN3BsHvfbwkugYfsgcjoiXzsbm8OiWczkbNQcadaYTHgIOPf9mNLLJXq1vSfBVLyDY+1Xq + + + ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc. + MIIDdDCCAlygAwIBAgIGAXo6K+u/MA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ + + + + + + + + idp_entity_id + + user_email + + + + + + + https://saml.boxyhq.com + + + + + user_email + + + user_firstName + + + user_lastName + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified + + + + \ No newline at end of file diff --git a/pages/api/apps/metadata.ts b/pages/api/apps/metadata.ts index 7bdf6ba..8136ce2 100644 --- a/pages/api/apps/metadata.ts +++ b/pages/api/apps/metadata.ts @@ -1,7 +1,5 @@ -import { promises as fs } from 'fs'; import type { NextApiRequest, NextApiResponse } from 'next'; -import path from 'path'; -import { metadata } from '../../../services'; +import { createCertificate, createIdPMetadataXML } from '../../../utils'; export default async function handler( req: NextApiRequest, @@ -12,12 +10,17 @@ export default async function handler( } async function download(req: NextApiRequest) { - const { acs_url, entity_id } = req.body; + const { acs_url, sp_entity_id } = req.body; - const certificateFilePath = path.join('data', 'x509cert.txt'); - const certificate = await fs.readFile(certificateFilePath, 'utf8'); + const certificate = await createCertificate(); + const idpEntityId = 'http://localhost:4000/sso'; + const idpSsoUrl = 'http://localhost:4000/sso'; - const xml = await metadata.createXML(acs_url, entity_id, certificate); + const xml = await createIdPMetadataXML({ + idpEntityId, + idpSsoUrl, + certificate, + }); res.setHeader('Content-type', 'text/xml'); res.setHeader('Content-Disposition', 'attachment; filename="metadata.xml"'); diff --git a/pages/api/apps/saml.ts b/pages/api/apps/saml.ts index 460748b..22534e8 100644 --- a/pages/api/apps/saml.ts +++ b/pages/api/apps/saml.ts @@ -1,39 +1,33 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import xml2js from 'xml2js'; - -const parseXML = (xml: string): Promise> => { - return new Promise((resolve, reject) => { - xml2js.parseString(xml, (err: Error, result: any) => { - resolve(result); - }); - }); -}; - -const extractSAMLRequestAttribute = async (SAMLRequest: string | string[]) => { - // @ts-ignore - const result = await parseXML(Buffer.from(SAMLRequest, 'base64').toString()); - const sp = result['samlp:AuthnRequest']['$']; - - return { - ID: sp['ID'], - IssueInstant: sp['IssueInstant'], - AssertionConsumerServiceURL: sp['AssertionConsumerServiceURL'], - ProviderName: sp['ProviderName'], - }; -}; +import { User } from '../../../types'; +import { + createSAMLResponseXML, + extractSAMLRequestAttributes, +} from '../../../utils'; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - if (req.method === 'GET') { + if (req.method === 'POST') { return await response(req); } + if (req.method === 'GET') { + const user: User = { + id: '1', + email: 'kiran@demo.com', + firstName: 'Kiran', + lastName: 'K', + }; + + return res.status(200).json(await createSAMLResponseXML(user)); + } + async function response(req: NextApiRequest) { const { RelayState, SAMLRequest } = req.query; - const attributes = await extractSAMLRequestAttribute(SAMLRequest); + const attributes = await extractSAMLRequestAttributes(SAMLRequest); return res.status(200).json(attributes); } diff --git a/pages/apps/saml.tsx b/pages/apps/saml.tsx new file mode 100644 index 0000000..7813ee7 --- /dev/null +++ b/pages/apps/saml.tsx @@ -0,0 +1,42 @@ +import type { NextPage } from 'next'; +import { useEffect, useRef, useState } from "react"; + +export async function getServerSideProps(context: any) { + const {RelayState, SAMLRequest} = context.query; + + return { + props: { + RelayState: RelayState, + SAMLRequest: SAMLRequest + }, + } +} + +// const createSAMLResponse = ({RelayState, SAMLRequest}: Prop) => { +// const url = new URL('http://28a2-103-153-104-43.ngrok.io/sso/acs'); + +// url.searchParams.append('RelayState', RelayState); +// url.searchParams.append('SAMLResponse', 'SAMLResponse'); + +// return url.href; +// } + +const SAML: NextPage = (prop) => { + const [] = useState(); + const formRef = useRef(null); + + useEffect(() => { + // @ts-ignore + formRef?.current?.submit(); + }, []); + + return ( +
+
+ +
+
+ ); +}; + +export default SAML; \ No newline at end of file diff --git a/services/metadata.ts b/services/metadata.ts index 1b7d02d..6975b34 100644 --- a/services/metadata.ts +++ b/services/metadata.ts @@ -1,4 +1,3 @@ -import * as xmlbuilder from 'xmlbuilder'; import type { IdPMetadata } from '../types'; const baseUrl = 'http://localhost:3000/saml'; @@ -19,50 +18,3 @@ export const create = ( certificate: certificate, }; }; - -const extractCert = (certificate: string) => { - return certificate - .replace('-----BEGIN CERTIFICATE-----', '') - .replace('-----END CERTIFICATE-----', '') - .trim(); -}; - -export const createXML = async ( - acs_url: string, - entity_id: string, - certificate: string -) => { - const metadata = create(acs_url, entity_id, certificate); - - const data = { - 'md:EntityDescriptor': { - '@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', - '@entityID': `${metadata.entity_id}`, - '@validUntil': '2026-06-22T18:39:53.000Z', - 'md:IDPSSODescriptor': { - '@WantAuthnRequestsSigned': 'false', - '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', - 'md:KeyDescriptor': { - '@use': 'signing', - 'ds:KeyInfo': { - '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', - 'ds:X509Data': { - 'ds:X509Certificate': { - '#text': `${extractCert(certificate)}`, - }, - }, - }, - }, - 'md:NameIDFormat': { - '#text': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', - }, - 'md:SingleSignOnService': { - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - '@Location': `${metadata.sso_url}`, - }, - }, - }, - }; - - return xmlbuilder.create(data).end({ pretty: true }); -}; diff --git a/types/index.ts b/types/index.ts index 4136abc..3f8efcb 100644 --- a/types/index.ts +++ b/types/index.ts @@ -30,3 +30,10 @@ export type AuthNRequest = { RelayState: string; SAMLRequest: SAMLRequest; }; + +export type User = { + id: string; + email: string; + firstName: string; + lastName: string; +}; diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 0000000..719cd59 --- /dev/null +++ b/utils/index.ts @@ -0,0 +1,84 @@ +// @ts-ignore +import { promises as fs } from 'fs'; +import path from 'path'; +import xml2js from 'xml2js'; +import { User } from '../types'; + +// Parse XML +const parseXML = (xml: string): Promise> => { + return new Promise((resolve, reject) => { + xml2js.parseString(xml, (err: Error, result: any) => { + resolve(result); + }); + }); +}; + +// Extract SAML Request Attributes +const extractSAMLRequestAttributes = async (SAMLRequest: string | string[]) => { + // @ts-ignore + const result = await parseXML(Buffer.from(SAMLRequest, 'base64').toString()); + const attributes = result['samlp:AuthnRequest']['$']; + + return { + ID: attributes['ID'], + IssueInstant: attributes['IssueInstant'], + AssertionConsumerServiceURL: attributes['AssertionConsumerServiceURL'], + ProviderName: attributes['ProviderName'], + }; +}; + +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', 'x509cert.txt'); + + return await fs.readFile(certificateFilePath, 'utf8'); +}; + +const extractCert = (certificate: string) => { + return certificate + .replace('-----BEGIN CERTIFICATE-----', '') + .replace('-----END CERTIFICATE-----', '') + .trim(); +}; + +// Create SAML Response XML +const createSAMLResponseXML = async (user: User): Promise => { + const xmlPath = path.join('data', 'saml-response.xml'); + const xml = await fs.readFile(xmlPath, 'utf8'); + + return xml + .replace( + /idp_entity_id/g, + 'https://accounts.google.com/o/saml2?idpid=C02frd9s1' + ) + .replace('sp_acs_url', 'some-url') + .replace(/user_email/g, 'kiran@demo.com') + .replace('user_firstName', 'Kiran') + .replace('user_lastName', 'K'); +}; + +export { + parseXML, + extractSAMLRequestAttributes, + createIdPMetadataXML, + createSAMLResponseXML, + createCertificate, + extractCert, +};