近期帮甲方开发+部署一个 SAML2.0 到 OAuth2.0 的转换服务,记录一下踩坑过程。
不得不吐槽一下 SAML2.0 协议实在是臃肿和繁琐。

For AD FS

我对接的甲方用微软 AD FS 作为 SAML2.0 的 IdP。需要注意以下几点:

  • 配置SP时会要求回调终结点/metadata的URL必须是HTTPS的
  • 注意看看IdP的metadata中对摘要签名算法的要求,区分SHA1和SHA256

Responder error: NoAuthnContext

在使用AD FS作为IdP时,可能会遇到这个错误,由于我没法设置AD FS的配置,所以只能在SP这边做处理。
SP可以不请求AuthnContext,这样就不会出现这个错误(显然有点野蛮)。
或者请求多种AuthnContext,并使用更宽松的匹配方式,这样就不会因为IdP返回的AuthnContext不匹配而报错。
由于node-saml的默认配置是exact,所以很容易出现这个问题,可以修改配置来解决,
比如在 node-saml 中可以这样配置:

1
2
3
4
5
6
7
8
9
new SAML({
racComparison: 'minimum', // 使用最小匹配方式
authnContext: [
'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
'urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'
]
// disableRequestedAuthnContext: true // 关闭请求AuthnContext
})

详情可以看看相关 GitHub Issue

Responder error: unspecified

这个错误信息太过于宽泛,基本无法定位问题。
最好的办法是查看IdP的日志,看看具体的错误信息。

我遇到的情况是IdP返回的StatusCodeurn:oasis:names:tc:SAML:2.0:status:Responder,并且没有StatusMessage,因此为unspecified
这种情况下,可能是IdP的配置有问题,比如证书不匹配、签名算法不匹配等等。
最终排查发现是摘要算法不匹配导致的,IdP的 metadata 中要求使用SHA256,而node-saml这个库并不会根据 IdP metadata 自动配置,因此还在使用默认的SHA1
手动修改配置后问题解决。(所以前文提到要注意看看IdP的metadata)

1
2
3
4
new SAML({
signatureAlgorithm: 'sha256',
digestAlgorithm: 'sha256'
})

Invalid Signature

前面签名算法都已经改了为什么还会报错呢?看了一下传入assert端点的SAMLResponse,发现里面已经有了登陆用户的断言信息了,相当于是已经过了IdP这关,那可以定位问题就出在SP这边。
同样,查到 (GitHub Issue)[https://github.com/node-saml/passport-saml/issues/816] 得知,新版 node-saml 默认会校验断言签名以及AuthnResponse签名。
但是在我的场景下,较古老的IdP返回的SAMLResponse中并没有对AuthnResponse签名,因此 SP 端签名校验会报错。
只需要关闭这个校验即可:

1
2
3
new SAML({
wantAuthnResponseSigned: false
})

注意: 还有一个选项是wantAssertionsSigned,这个是控制是否校验断言签名的。不过 wantAuthnResponseSignedwantAssertionsSigned 二者最多只能关闭一个,否则会报错。因此不能想着偷懒把两个都关闭。

无法获取用户信息

签名校验通过后,node-saml 库会解析断言信息,但是在我的场景下,解析后的用户信息为空。
发现断言信息中用的不是标准的 nameID,而是中文的名称ID(也属实逆天了),因此需要手动配置:

1
2
3
4
const username = (profile['名称ID'] as string) ?? profile.nameID;
if (!username) {
return Promise.reject(new Error('Invalid profile: blank nameID'));
}

SAML assertion not yet valid

这个错误通常是由于 IdP 和 SP 的时间不同步导致的(IdP 时间比 SP 时间快,生成了未来的断言),可以通过同步时间解决。
不过由于我没法设置 IdP 的系统时间,因此只能通过让 SP 等待一段时间或者让 SP 允许时间偏差:

1
2
3
new SAML({
acceptedClockSkewMs: 5000 // 5s
})