You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Node.js中生成IdP SAML响应并实现IdP发起SSO

基于Node.js的IdP发起式SAML 2.0 SSO最小可行示例

一、前置准备:密钥对与元数据配置

1. 生成RSA密钥对

SAML 2.0要求使用密钥对进行签名/加密,用OpenSSL生成:

# 生成IdP私钥和公钥
openssl genrsa -out idp-private-key.pem 2048
openssl rsa -in idp-private-key.pem -pubout -out idp-public-key.pem

# 生成SP私钥和公钥
openssl genrsa -out sp-private-key.pem 2048
openssl rsa -in sp-private-key.pem -pubout -out sp-public-key.pem

2. 元数据文件核心内容

元数据是IdP和SP之间的"身份名片",必须包含以下核心字段:

IdP元数据(idp-metadata.xml)

<md:EntityDescriptor entityID="https://localhost:3000/idp/metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
  <md:IDPSSODescriptor 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>【替换为idp-public-key.pem内容,去掉首尾的PEM标识】</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:3000/initiate-sso"/>
  </md:IDPSSODescriptor>
</md:EntityDescriptor>

SP元数据(sp-metadata.xml)

<md:EntityDescriptor entityID="https://localhost:3001/sp/metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
  <md:SPSSODescriptor 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>【替换为sp-public-key.pem内容,去掉首尾的PEM标识】</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:3001/sso/acs" index="1"/>
  </md:SPSSODescriptor>
</md:EntityDescriptor>

二、samlify代码实现(解决你遇到的错误)

1. IdP服务代码(idp.js)

const express = require('express');
const samlify = require('samlify');
const fs = require('fs');

const app = express();
app.use(express.urlencoded({ extended: true }));

// 加载配置文件
const idpMetadata = fs.readFileSync('./idp-metadata.xml', 'utf8');
const spMetadata = fs.readFileSync('./sp-metadata.xml', 'utf8');
const idpPrivateKey = fs.readFileSync('./idp-private-key.pem', 'utf8');

// 初始化IdP和SP实例
const idp = samlify.IdentityProvider({
  metadata: idpMetadata,
  privateKey: idpPrivateKey,
  privateKeyPass: '' // 私钥无密码则留空
});

const sp = samlify.ServiceProvider({
  metadata: spMetadata
});

// IdP发起SSO的入口
app.get('/initiate-sso', async (req, res) => {
  try {
    // 指定POST绑定,解决ERR_CREATE_RESPONSE_UNDEFINED_BINDING错误
    const { context } = await idp.createLoginResponse(sp, 'post', {
      nameID: 'user_001',
      nameIDFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
      attributes: {
        email: 'user@example.com',
        role: 'admin'
      }
    });
    // 自动提交表单跳转至SP的断言消费端点
    res.send(`
      <html>
        <body onload="document.forms[0].submit()">
          <form method="post" action="${context}">
            <input type="hidden" name="SAMLResponse" value="${context.SAMLResponse}" />
          </form>
        </body>
      </html>
    `);
  } catch (err) {
    console.error('SSO响应生成失败:', err);
    res.status(500).send('发起SSO失败');
  }
});

app.listen(3000, () => console.log('IdP运行在http://localhost:3000'));

2. SP服务代码(sp.js)

const express = require('express');
const samlify = require('samlify');
const fs = require('fs');

const app = express();
app.use(express.urlencoded({ extended: true }));

// 加载配置文件
const spMetadata = fs.readFileSync('./sp-metadata.xml', 'utf8');
const idpMetadata = fs.readFileSync('./idp-metadata.xml', 'utf8');
const spPrivateKey = fs.readFileSync('./sp-private-key.pem', 'utf8');

// 初始化SP和IdP实例
const sp = samlify.ServiceProvider({
  metadata: spMetadata,
  privateKey: spPrivateKey
});

const idp = samlify.IdentityProvider({
  metadata: idpMetadata
});

// SP的断言消费端点,接收IdP的SAML响应
app.post('/sso/acs', async (req, res) => {
  try {
    const { extract } = await sp.parseLoginResponse(idp, 'post', req);
    // 解析到的用户身份信息
    res.send(`
      <h1>登录成功</h1>
      <p>用户ID: ${extract.nameID}</p>
      <p>邮箱: ${extract.attributes.email}</p>
      <p>角色: ${extract.attributes.role}</p>
    `);
  } catch (err) {
    console.error('SAML响应解析失败:', err);
    res.status(401).send('登录验证失败');
  }
});

app.listen(3001, () => console.log('SP运行在http://localhost:3001'));

三、错误排查指南

  • ERR_CREATE_RESPONSE_UNDEFINED_BINDING:确保createLoginResponse的第二个参数是post或redirect(小写),且元数据中存在对应绑定的服务地址。
  • ERR_CRYPTO_SIGN_KEY_REQUIRED:检查IdP实例是否正确加载私钥,文件路径是否正确,私钥为标准PEM格式。
  • 元数据不匹配:确认IdP和SP的实体ID、服务地址、公钥完全对应,无拼写错误。

四、替代Node.js库推荐

如果samlify不符合需求,可尝试:

  • passport-saml:基于Passport.js的SAML实现,适合已有Passport认证体系的项目。
  • saml2-js:轻量级SAML 2.0库,API简洁,文档清晰。

内容的提问来源于stack exchange,提问作者Mykyta Manuilenko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 10:23:08