如何在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
相关产品推荐
相关产品推荐

