如何在node.js中生成身份提供商(IdP)SAML响应?

问题描述 投票:0回答:1

我有两个非常简单的node.js应用程序:

idp
(身份提供商)和
sp
(服务提供商)。这些应用程序没有任何特定的业务逻辑,我只想在node.js 中创建一个非常简单的单点登录(SSO)示例。更具体地说,我想实现身份提供商 (IdP) 发起的单点登录 (SSO),但我不明白如何生成 SAML IdP 响应,而且,我不明白什么是 IdP/SP元数据文件应该有(我是 SSO 和 SAML 2.0 协议的新手)。

我有以下 Node.js 服务器:

身份提供商 (IdP) 服务器:

/*
 * This is the code in the idp.js file
 */
const express = require('express');
const saml = require('samlify');
const fs = require('fs');

const app = express();
const port = 3000;

const idp = saml.IdentityProvider({
  metadata: fs.readFileSync(__dirname + '/idp-metadata.xml')
});

const sp = saml.ServiceProvider({
  metadata: fs.readFileSync(__dirname + '/sp-metadata.xml')
});

app.get('/metadata', (req, res) => {
  res.type('application/xml');
  res.send(idp.getMetadata());
});

/*
 * The endpoint that is used to initiate IdP SSO.
 */
app.get('/idpinitsso', async (req, res) => {
  try {
    // As far as I understand when this endpoint is called I need to generate SAML Response 
    // and then send an HTTP POST request with SAMLResponse url-encoded body. 
    // But the question is how to generate SAML Response?
  } catch (e) {
    console.log(e)
  }

  res.status(204).send()
})

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

服务提供商(SP)服务器:

/*
 * This is the code in the sp.js file
 */
const express = require('express');
const saml = require('samlify');
const fs = require('fs');

const app = express();
const port = 3001;

const sp = saml.ServiceProvider({
  metadata: fs.readFileSync(__dirname + '/sp-metadata.xml')
});

const idp = saml.IdentityProvider({
  metadata: fs.readFileSync(__dirname + '/idp-metadata.xml')
});

app.get('/metadata', (req, res) => {
  res.type('application/xml');
  res.send(sp.getMetadata());
});

app.post('/acs', async (req, res) => {
  const parseResult = sp.parseLoginResponse(idp, 'post', req)
  console.log(parseResult)

  res.status(204).send()
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

idp-metadata.xml(此 XML 取自 https://samlify.js.org/#/idp + https://samlify.js.org/#/key- Generation,用于生成X509证书)

<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3000/metadata">
  <IDPSSODescriptor xmlns:ds="http://www.w3.org/2000/09/xmldsig#" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <KeyDescriptor use="signing">
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>MIIFeDCCA2ACCQDM8Gu+flnutzANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMQ0wCwYDVQQKDARBeG9uMQ0wCwYDVQQLDARBeG9uMQ0wCwYDVQQDDARBeG9uMRwwGgYJKoZIhvcNAQkBFg1uaWtAZ21haWwuY29tMB4XDTIzMTExMzE5MTkwMVoXDTMzMTExMDE5MTkwMVowfjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9yazENMAsGA1UECgwEQXhvbjENMAsGA1UECwwEQXhvbjENMAsGA1UEAwwEQXhvbjEcMBoGCSqGSIb3DQEJARYNbmlrQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANK68twNmdNIYN5+Au/fNOM5f7JTlet/Cyxb4znmK51YNDYioROdvk1Z1Zvn7eZZrwA34ID/sMnHxKHMksdTSbgAmXox50LLJpx4/kTWwzw/NH1IvXD137nBfkcdr41pe56+i6uc5O2yWbzaVMzZKbEC448lkL6bFAoc3s5aRL1YVlPZsHolItINZReBCW80jdEwT1lI0jQTuD3qRaU3QPbNaT/RY39NGdVkuOoBWICyyvO6N7HHwv+UmIlLzvQH1gLYI2+pDbTyH33lUDnslN5tch5/x/m/TZFek2KpZQ2gIoihYtrZscvHzYVsaeW5A/PEqvsOfQMHbpzFktlfufZ/dcgV8lBey36itxp82/DW5SEQmZBUqnoISVTNiq1j2goALvoF5l+lnQtkyJCRACayln/U7z4ktaTJGxs/O9eXkXsi+FTOmWVWn0NRCmHQTERX+3zCreLExMGTCLSNPKBRbyo0ydYsHR55GkkxCQbwRy671hkm0W4yF/YkDcW2WIFF2bvSy0/wCHFTU0PxzIl07vwlMejIaYibW8cv+hxa9nLvhilvpZ9wPFaLaMzWKsPcYDgnic/W/3niy2uSGrH5uLBPax3jb3cyLiFNEdUAEdYLOhGco0WWDbUEUMZOhGBlF7M4wtnti+94F4zqW76QRT6WBk8S9Au8Fk/B6fQTAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAAu1lt88IOsQXHnpFSP7ewK1GOjxiNI/k6mYiGT4OowCjBDmeX06/OnVSP58JkdnJUwSRC9f3iblvAD02NyY9IRjGvPPEUgA6G2zmcrTt72XyZIMYh1yDyLdMuWQAtRQvs75x9MeQWHe7wN5XXkoazSoLxCmyZs8LzYoGwnMxdjO6gq4A/DwXklplMUXSoj3rTbKDXi65CxFzDyEkYPlqJrRE3N7DKCBtuhp5m+EQJZeeCEKBxahhoww1QV5K+qHbMo8Hjg89b+8o82YRYXLcaCYQ9tJayXadx2qk9RghpAhG2TNVZpegPzM9UAJ0bFgh1O4v/oc5QiywRuEEhzO8Ml4fCJ3y3MQBJ/7ESnkJtQZkaErT4TYT8i3hkZL5HPeIZ2/NQbc+DYDyZQQVWy8M26rBQYTEqNuWQCaXKJr03vc2MyXKgZ8Hr/JywzRJOnCvBdSwvu8PffgJsYgexwmU1dwMQQAdA1utJkayOQTsc24YFOIDV4a+p3cvD1GeUDON7swJKnyKX4XBQFejMp5kG7V1p2KB5s0aDOJ+twDnpPCALX3Zs07PScd0H/wiDQkjI/ZQpEMO/2BTH5D9/x40vhnA4olVHRwuDV+xLvi0fwtQfA6T10f7cl9GWfjX0lsf95Qa1u0tj9BjQ2XWcncY5po31Q6aEOLK6biDxlE5kd1</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </KeyDescriptor>
    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/trust/saml2/http-post/sso/486670"/>
    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3000/trust/saml2/http-post/sso/486670"/>
    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://localhost:3000/trust/saml2/soap/sso/486670"/>
  </IDPSSODescriptor>
  <ContactPerson contactType="technical">
    <SurName>Support</SurName>
    <EmailAddress>[email protected]</EmailAddress>
  </ContactPerson>
</EntityDescriptor>

sp-metadata.xml(此 XML 取自 https://samlify.js.org/#/sp + https://samlify.js.org/#/key- Generation,用于生成X509证书)

<EntityDescriptor
 xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
 xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
 entityID="https://sp.example.org/metadata">
    <SPSSODescriptor WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
        <KeyDescriptor use="signing">
            <KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <X509Data>
                    <X509Certificate>MIIFeDCCA2ACCQDM8Gu+flnutzANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMQ0wCwYDVQQKDARBeG9uMQ0wCwYDVQQLDARBeG9uMQ0wCwYDVQQDDARBeG9uMRwwGgYJKoZIhvcNAQkBFg1uaWtAZ21haWwuY29tMB4XDTIzMTExMzE5MTkwMVoXDTMzMTExMDE5MTkwMVowfjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9yazENMAsGA1UECgwEQXhvbjENMAsGA1UECwwEQXhvbjENMAsGA1UEAwwEQXhvbjEcMBoGCSqGSIb3DQEJARYNbmlrQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANK68twNmdNIYN5+Au/fNOM5f7JTlet/Cyxb4znmK51YNDYioROdvk1Z1Zvn7eZZrwA34ID/sMnHxKHMksdTSbgAmXox50LLJpx4/kTWwzw/NH1IvXD137nBfkcdr41pe56+i6uc5O2yWbzaVMzZKbEC448lkL6bFAoc3s5aRL1YVlPZsHolItINZReBCW80jdEwT1lI0jQTuD3qRaU3QPbNaT/RY39NGdVkuOoBWICyyvO6N7HHwv+UmIlLzvQH1gLYI2+pDbTyH33lUDnslN5tch5/x/m/TZFek2KpZQ2gIoihYtrZscvHzYVsaeW5A/PEqvsOfQMHbpzFktlfufZ/dcgV8lBey36itxp82/DW5SEQmZBUqnoISVTNiq1j2goALvoF5l+lnQtkyJCRACayln/U7z4ktaTJGxs/O9eXkXsi+FTOmWVWn0NRCmHQTERX+3zCreLExMGTCLSNPKBRbyo0ydYsHR55GkkxCQbwRy671hkm0W4yF/YkDcW2WIFF2bvSy0/wCHFTU0PxzIl07vwlMejIaYibW8cv+hxa9nLvhilvpZ9wPFaLaMzWKsPcYDgnic/W/3niy2uSGrH5uLBPax3jb3cyLiFNEdUAEdYLOhGco0WWDbUEUMZOhGBlF7M4wtnti+94F4zqW76QRT6WBk8S9Au8Fk/B6fQTAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAAu1lt88IOsQXHnpFSP7ewK1GOjxiNI/k6mYiGT4OowCjBDmeX06/OnVSP58JkdnJUwSRC9f3iblvAD02NyY9IRjGvPPEUgA6G2zmcrTt72XyZIMYh1yDyLdMuWQAtRQvs75x9MeQWHe7wN5XXkoazSoLxCmyZs8LzYoGwnMxdjO6gq4A/DwXklplMUXSoj3rTbKDXi65CxFzDyEkYPlqJrRE3N7DKCBtuhp5m+EQJZeeCEKBxahhoww1QV5K+qHbMo8Hjg89b+8o82YRYXLcaCYQ9tJayXadx2qk9RghpAhG2TNVZpegPzM9UAJ0bFgh1O4v/oc5QiywRuEEhzO8Ml4fCJ3y3MQBJ/7ESnkJtQZkaErT4TYT8i3hkZL5HPeIZ2/NQbc+DYDyZQQVWy8M26rBQYTEqNuWQCaXKJr03vc2MyXKgZ8Hr/JywzRJOnCvBdSwvu8PffgJsYgexwmU1dwMQQAdA1utJkayOQTsc24YFOIDV4a+p3cvD1GeUDON7swJKnyKX4XBQFejMp5kG7V1p2KB5s0aDOJ+twDnpPCALX3Zs07PScd0H/wiDQkjI/ZQpEMO/2BTH5D9/x40vhnA4olVHRwuDV+xLvi0fwtQfA6T10f7cl9GWfjX0lsf95Qa1u0tj9BjQ2XWcncY5po31Q6aEOLK6biDxlE5kd1</X509Certificate>
                </X509Data>
            </KeyInfo>
        </KeyDescriptor>
        <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
        <AssertionConsumerService isDefault="true" index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3001/acs"/>
    </SPSSODescriptor>
</EntityDescriptor>

我尝试使用

idp.createLoginResponse(sp)
但它对我不起作用,因为我收到了
ERR_CREATE_RESPONSE_UNDEFINED_BINDING
错误。

我也尝试过这样生成它:

const user = { email: '[email protected]' };
const sampleRequestInfo = { extract: { request: { id: 'request_id' } } };

const samlResponse = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user);

但是我收到以下错误:

"Error [ERR_CRYPTO_SIGN_KEY_REQUIRED]: No key provided for signing."

我在 https://samlify.js.org/ 中找不到使用 IdP 发起的 SSO 的工作示例,因此如果您能为我提供一个最小的工作示例或建议任何好的节点,我将非常感激。可以处理此问题的 Node.js 库(或仅具有 IdP 发起的 SSO 示例)或任何其他语言的任何其他示例。

node.js single-sign-on saml-2.0 idp samlify
1个回答
0
投票

嗯,经过几天的奋斗,我终于明白了SSO是如何工作的并实现了它(IdP发起的SSO,我们的服务充当第三方服务的身份提供商)。我会在这里留下我的答案,以防有人必须做同样的事情并遇到问题。

首先,我建议您阅读 SSO 流程的工作原理。我将在这里留下一些很好的资源供您阅读:

  1. http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0-cd-02.html#5.1.1.简介|概要
  2. https://developer.okta.com/docs/concepts/saml/

因此,简而言之,IdP 发起的 SSO 流程(您的服务充当 IdP)的工作原理如下:

  1. 用户已登录身份提供商(您的服务)。
  2. 用户将转到具有集成的页面(或用户可以开始与服务提供商集成的任何页面)
  3. 用户单击“登录到[服务提供商名称]”之类的内容。
  4. IdP 生成
    SAMLResponse
  5. IdP 向服务提供商的 ACS(断言消费者服务)URL 发送带有
    SAMLResponse
    的 HTTP POST 请求。

在开始编写代码之前,您需要执行以下操作:

  1. 向您的服务提供商索取 XML 元数据文件(SP 元数据)。
  2. 创建您自己的 XML 元数据文件(IdP 元数据)并将其发送给您的服务提供商。

您可以在此处创建您自己的元数据文件(IdP 元数据):https://www.samltool.com/idp_metadata.php.

强烈建议对 XML 元数据文件进行签名,因此通过运行以下命令创建文件

private_key.pem
public_cert.cer

openssl genrsa -passout pass:jXmKf9By6ruLnUdRo90G -out private_key.pem 4096
openssl req -new -x509 -key private_key.pem -out public_cert.cer -days 3650

在此处创建 IdP 元数据文件时,您应该将

public_cert.cer
中的正文添加到“SP X.509 证书(用于签名/加密的相同证书)”字段:https://www.samltool.com/idp_metadata。 php

最后,当一切准备就绪(您拥有 SP 和 IdP 元数据文件)时,我们可以继续创建 IdP。

我们的 IdP 是用 Node.js 编写的,因此我们使用 samlify 来创建 SAMLResponse:

/**
 * idp.js
 */
const express = require('express');
const saml = require('samlify');

const { addMinutes } = require('date-fns')
const { readFileSync } = require('fs');
const { randomUUID } = require('crypto');

const app = express();
const port = 3000;

const generateRequestID = () => {
    return '_' + randomUUID()
}

// Unfortunately as it's said in https://github.com/tngan/samlify/issues/373
// to add attributes to your SAMLResponse using samlify you need to
// completely duplicate the logic from here: https://github.com/tngan/samlify/blob/2e1a93671f4b98980472d4857e08a2f99a236acd/src/binding-post.ts#L93-L123
const createTemplateCallback = (idp, sp, email) => template => {
    const assertionConsumerServiceUrl = sp.entityMeta.getAssertionConsumerService(saml.Constants.wording.binding.post)

    const nameIDFormat = idp.entitySetting.nameIDFormat
    const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat

    const id = generateRequestID()
    const now = new Date()
    const fiveMinutesLater = addMinutes(now, 5)

    const tagValues = {
        ID: id,
        AssertionID: generateRequestID(),
        Destination: assertionConsumerServiceUrl,
        Audience: sp.entityMeta.getEntityID(),
        EntityID: sp.entityMeta.getEntityID(),
        SubjectRecipient: assertionConsumerServiceUrl,
        Issuer: idp.entityMeta.getEntityID(),
        IssueInstant: now.toISOString(),
        AssertionConsumerServiceURL: assertionConsumerServiceUrl,
        StatusCode: 'urn:oasis:names:tc:SAML:2.0:status:Success',
        ConditionsNotBefore: now.toISOString(),
        ConditionsNotOnOrAfter: fiveMinutesLater.toISOString(),
        SubjectConfirmationDataNotOnOrAfter: fiveMinutesLater.toISOString(),
        NameIDFormat: selectedNameIDFormat,
        NameID: email,
        InResponseTo: 'null',
        AuthnStatement: '',
        /**
         * Custom attributes
         */
        attrFirstName: 'Jon',
        attrLastName: 'Snow',
    }

    return {
        id,
        context: saml.SamlLib.replaceTagsByValue(template, tagValues)
    }
}

/*
 * Service Provider config (required for creating SAMLResponse)
 */
const sp = saml.ServiceProvider({
    metadata: readFileSync(__dirname + '/metadata/sp-metadata.xml')
});

/*
 * Identity Provider config
 */
const idp = saml.IdentityProvider({
    metadata: readFileSync(__dirname + '/metadata/idp-metadata.xml'),
    privateKey: readFileSync(__dirname + '/key/idp/private_key.pem'),
    privateKeyPass: 'jXmKf9By6ruLnUdRo90G',
    isAssertionEncrypted: false,
    loginResponseTemplate: {
        context: '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AttributeStatement}</saml:Assertion></samlp:Response>',
        attributes: [
            { name: 'firstName', valueTag: 'firstName', nameFormat: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', valueXsiType: 'xs:string' },
            { name: 'lastName', valueTag: 'lastName', nameFormat: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', valueXsiType: 'xs:string' },
        ],
    }
});

app.get('/api/sso/saml2/idp/metadata', (req, res) => {
    res.type('application/xml');
    res.send(idp.getMetadata());
});

app.post('/api/sso/saml2/idp/login', async (req, res) => {
    try {
        const user = { email: '[email protected]' };
        const { context, entityEndpoint } = await idp.createLoginResponse(sp, null, saml.Constants.wording.binding.post, user, createTemplateCallback(idp, sp, user.email));

        res.status(200).send({ samlResponse: context, entityEndpoint })
    } catch (e) {
        console.log(e)
        res.status(500).send()
    }
})

app.listen(port, () => {
    console.log(`Identity Provider server listening at http://localhost:${port}`);
});

启动服务器并向

http://localhost:3000/api/sso/saml2/idp/login
发送HTTP POST请求,您将收到以下JSON响应正文:

{
    "samlResponse": "base64-encoded string",
    "entityEndpoint": "http://localhost:3001/api/sso/saml2/sp/acs"
}

之后,将内容类型为

application/x-www-form-urlencoded
的 HTTP POST 请求发送到
entityEndpoint
,正文为
SAMLResponse

© www.soinside.com 2019 - 2024. All rights reserved.