diff --git a/package-lock.json b/package-lock.json index 4dc4a5c..dda2f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.3.2", "license": "Apache 2.0", "dependencies": { - "@boxyhq/saml20": "1.4.5", + "@boxyhq/saml20": "1.4.9", "daisyui": "4.6.1", "next": "14.1.0", "react": "18.2.0", @@ -248,9 +248,9 @@ } }, "node_modules/@boxyhq/saml20": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@boxyhq/saml20/-/saml20-1.4.5.tgz", - "integrity": "sha512-iI+pEmCzl3r0RrIZ6L6QwuCoCZAkkcYTLJZbHLwNxll5JuDEXwg/tZu4Df7j5rO08aggf1w6cpIf/Ng/6hhLLw==", + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@boxyhq/saml20/-/saml20-1.4.9.tgz", + "integrity": "sha512-S86wTvQkFDbCuwCeyu3AUbHUh3qWtqge557RwIeo2T0L3kL2xMeOJK1QcRBEfborxUXdL9zRmn5sUqceCuWYjQ==", "dependencies": { "@xmldom/xmldom": "0.8.10", "lodash": "4.17.21", diff --git a/package.json b/package.json index 52dcd36..60018b0 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "release": "git checkout release && git merge origin/main && release-it && git checkout main && git merge origin/release && git push origin main" }, "dependencies": { - "@boxyhq/saml20": "1.4.5", + "@boxyhq/saml20": "1.4.9", "daisyui": "4.6.1", "next": "14.1.0", "react": "18.2.0", diff --git a/pages/api/saml/auth.ts b/pages/api/saml/auth.ts index fa3accf..55244a6 100644 --- a/pages/api/saml/auth.ts +++ b/pages/api/saml/auth.ts @@ -2,7 +2,6 @@ import { createHash } from 'crypto'; import config from 'lib/env'; import type { NextApiRequest, NextApiResponse } from 'next'; import type { User } from 'types'; -import { createResponseXML, signResponseXML } from 'utils'; import saml from '@boxyhq/saml20'; import { getEntityId } from 'lib/entity-id'; @@ -24,15 +23,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) lastName: userName, }; - const xml = await createResponseXML({ - idpIdentityId: getEntityId(config.entityId, req.query.namespace as any), + const xmlSigned = await saml.createSAMLResponse({ + issuer: getEntityId(config.entityId, req.query.namespace as any), audience, acsUrl, - samlReqId: id, - user: user, + requestId: id, + claims: { + email: user.email, + raw: user, + }, + privateKey: config.privateKey, + publicKey: config.publicKey, }); - const xmlSigned = await signResponseXML(xml, config.privateKey, config.publicKey); const encodedSamlResponse = Buffer.from(xmlSigned).toString('base64'); const html = saml.createPostForm(acsUrl, [ { diff --git a/utils/index.ts b/utils/index.ts index 4805765..572dc75 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,3 +1,2 @@ export * from './certificate'; export * from './idp'; -export * from './response'; diff --git a/utils/response.ts b/utils/response.ts deleted file mode 100644 index 323dcf7..0000000 --- a/utils/response.ts +++ /dev/null @@ -1,150 +0,0 @@ -import crypto from 'crypto'; -import xmlbuilder from 'xmlbuilder'; -import { User } from '../types'; -import saml from '@boxyhq/saml20'; - -const responseXPath = - '/*[local-name(.)="Response" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]'; - -const randomId = () => { - return '_' + crypto.randomBytes(10).toString('hex'); -}; -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': randomId(), - '@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:nameid-format:entity', - '#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': randomId(), - '@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:SubjectConfirmation': { - '@Method': 'urn:oasis:names:tc:SAML:2.0:cm:bearer', - 'saml:SubjectConfirmationData': { - '@InResponseTo': inResponseTo, - '@NotOnOrAfter': notAfter, - '@Recipient': acsUrl, - }, - }, - }, - '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(); -}; - -const signResponseXML = async (xml: string, signingKey: any, publicKey: any): Promise => { - return await saml.sign(xml, signingKey, publicKey, responseXPath); -}; - -export { createResponseXML, signResponseXML };