如何验证Azure AD生成的SAML Response签名?Node.js实现遇阻
Azure AD SAML Response签名验证失败问题排查
问题描述
我需要验证Azure AD生成的SAML Response,已在Azure AD SSO设置中将“SAML Signing Option”配置为“SAML Response”。原始响应经Base64解码后是无空格的XML。我从XML中提取了SignatureValue和X509Certificate,使用Node.js的crypto模块编写了验证代码,但运行后始终返回“SAML Response signature is invalid”,想知道遗漏了哪些步骤。
现有验证代码
const fs = require('fs'); const crypto = require('crypto'); const samlResponse = '<samlp:Response...>...</samlp:Response>' //完整XML字符串 const signatureValue = '...' //<SignatureValue>标签内的值 const x509Certificate = '...' //<ds:X509Certificate>标签内的值 const cert = `-----BEGIN CERTIFICATE----- ${x509Certificate} -----END CERTIFICATE-----`; // 验证签名 const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(samlResponse, 'utf8'); const isSignatureValid = verifier.verify(cert, signatureValue, 'base64'); if (isSignatureValid) { console.log('SAML Response signature is valid.'); } else { console.error('SAML Response signature is invalid.'); }
SAML Response结构
<samlp:Response ID="_188301aa-efa1-405f-ad6d-cf0ae65e347b" Version="2.0" IssueInstant="2023-09-27T18:28:02.720Z" Destination="some destination..." xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">some... </Issuer> <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/> <Reference URI="#_188301aa-efa1-405f-ad6d-cf0ae65e347b"> <Transforms> <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> </Transforms> <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/> <DigestValue>...</DigestValue> </Reference> </SignedInfo> <SignatureValue>...</SignatureValue> <KeyInfo> <ds:X509Data xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Certificate>...</ds:X509Certificate> </ds:X509Data> </KeyInfo> </Signature> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </samlp:Status> <Assertion ID="_bcc97cde-fe0d-4f41-92df-4a32348c1c00" IssueInstant="2023-09-27T18:28:02.719Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"> <Issuer>...</Issuer> <Subject> <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">...</NameID> <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <SubjectConfirmationData NotOnOrAfter="2023-09-27T19:28:02.519Z" Recipient="..."/> </SubjectConfirmation> </Subject> <Conditions NotBefore="2023-09-27T18:23:02.519Z" NotOnOrAfter="2023-09-27T19:28:02.519Z"> <AudienceRestriction> <Audience>matansso</Audience> </AudienceRestriction> </Conditions> <AttributeStatement> ... </AttributeStatement> <AuthnStatement AuthnInstant="2023-09-19T13:39:36.939Z" SessionIndex="_bcc97cde-fe0d-4f41-92df-4a32348c1c00"> <AuthnContext> <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef> </AuthnContext> </AuthnStatement> </Assertion> </samlp:Response>
遗漏的关键步骤
1. 未遵循XML-DSig的转换与规范化规则
你的代码直接使用原始XML字符串进行验证,但根据SAML Response中的<SignedInfo>配置,签名计算前必须执行两个核心步骤:
- 信封式签名转换:移除整个
<Signature>节点(签名节点本身不参与签名计算) - 独占XML规范化(Exc-C14N):对移除签名节点后的XML执行严格规范化,包括命名空间排序、属性排序、空白字符处理等,必须完全符合
http://www.w3.org/2001/10/xml-exc-c14n#标准
手动实现这些规则极易出错,比如命名空间上下文保留、特殊字符处理等,这是验证失败的核心原因。
2. 直接使用crypto模块的局限性
Node.js的crypto模块仅支持原始字符串的签名验证,无法处理XML-DSig特有的转换和规范化逻辑,必须使用专门的XML签名库来处理这些步骤。
修正后的验证代码示例(使用xml-crypto)
const xmlCrypto = require('xml-crypto'); const samlResponse = '<samlp:Response...>...</samlp:Response>'; // 完整XML字符串 const x509Certificate = '...'; // <ds:X509Certificate>标签内的值 const responseId = '_188301aa-efa1-405f-ad6d-cf0ae65e347b'; // 对应Response的ID属性 // 初始化签名验证器 const sig = new xmlCrypto.SignedXml(); // 配置签名引用规则,与SAML中的<Reference>配置完全匹配 sig.addReference( `//*[@ID='${responseId}']`, ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"], "http://www.w3.org/2001/04/xmlenc#sha256" ); // 设置验证用的公钥证书 sig.signingKey = `-----BEGIN CERTIFICATE----- ${x509Certificate} -----END CERTIFICATE-----`; // 加载签名并验证 sig.loadSignature(samlResponse); const isValid = sig.checkSignature(samlResponse); if (isValid) { console.log('SAML Response签名验证通过'); } else { console.error('SAML Response签名验证失败,错误信息:', sig.validationErrors); }
额外注意事项
- 确保解码后的XML未被修改(如添加换行、空格),保持原始无空格状态
- 确认X509Certificate完整无截断,拼接PEM格式时每行64字符是标准要求(部分库会自动处理,但建议遵循)
- 验证时需同时检查SAML的其他属性,比如
Destination、NotBefore/NotOnOrAfter等,确保响应合法性
内容的提问来源于stack exchange,提问作者matan__2
相关产品推荐
相关产品推荐

