LabHub
学习 学习路径 课程

企业认证对接

OIDC 与 SAML — 校验什么才算安全

在 LabHub 中继续学习

一句话总结

无论 OIDC 还是 SAML,真正提供安全性的都不是协议名称,而是接收方的验证清单。签名、iss、aud、exp、nonce 中漏掉任何一项,攻击者都可能从那个缺口进入。

流程图: 接收方的验证清单 · 正常流程中仍会完美运行 · 是否知道完整清单 · 为什么要先换取一次 code。

为什么这是个问题

即使省略验证,集成在正常流程中仍会完美运行:能够登录,页面正常显示,用户名也正确。因此开发与集成测试期间不会出现任何信号。

遗漏的验证只会在两个时刻暴露:接受安全检测时,或事故发生时。不验证签名,任何人都能修改 payload 后冒充任意用户;不验证 aud,签发给其他客户端的令牌也能在本应用通过。这不是实现难度问题,而是是否知道完整清单的问题。

OIDC Authorization Code 流程

这是带浏览器的 Web 应用标准流程,共五步。

1. 사용자가 우리 앱의 보호된 페이지 요청
2. 앱 → 브라우저를 IdP 로 리다이렉트
   GET /authorize?response_type=code&client_id=labhub-web
                 &redirect_uri=https://app.example.com/callback
                 &scope=openid profile email
                 &state=<임의값>&nonce=<임의값>
3. IdP 가 로그인 처리 후 브라우저를 우리 앱으로 리다이렉트
   302 Location: https://app.example.com/callback?code=<인가코드>&state=<임의값>
4. 앱(서버)이 백채널로 토큰 교환
   POST /token  grant_type=authorization_code&code=...&redirect_uri=...
   → { access_token, id_token, refresh_token, expires_in }
5. 앱이 id_token 을 검증하고 세션을 만든다

为什么要先换取一次 code。 如果把令牌直接放进浏览器 URL,它会留在浏览历史、Referer 和日志中。授权码是一次性的,寿命很短;真正的令牌通过服务器到服务器的后端通道获取,因此更安全。

statenonce 的职责不同,经常被混淆。

ID 令牌必须验证什么

ID 令牌是 JWT,由点号连接 헤더.페이로드.서명 三部分,各自使用 base64url。任何人都能解码。 真正提供安全性的是签名验证。

验证项 不验证会发生什么
签名 任何人都能修改 payload 后以管理员身份登录
iss(签发方) 攻击者用自己的 IdP 签发令牌也能通过
aud(受众) 签发给其他应用的令牌也能通过(令牌替换)
exp(过期) 已过期令牌永远有效
nonce 旧令牌可以重复使用
alg 遭受 alg: none 或算法混淆攻击

最后一项是著名陷阱。如果 JWT 库信任令牌自称使用的算法,攻击者就能改成 alg: none,在没有签名的情况下通过。验证代码必须固定为我们预期的算法

实际项目还必然遇到时钟问题。验证 exp/iat 时,应允许大约 60~120 秒的 clock skew。一台 NTP 偏移的服务器就可能制造间歇性登录失败,而这种故障通常很难定位。

SAML 2.0——XML,签名决定一切

SAML 通过浏览器 POST 传递一个 XML 文档(Response 内含 Assertion),结构如下。

<samlp:Response Destination="https://app.example.com/saml/acs"
                InResponseTo="_req123" IssueInstant="...">
  <saml:Issuer>https://idp.example.com/</saml:Issuer>
  <samlp:Status><samlp:StatusCode Value="...:Success"/></samlp:Status>
  <saml:Assertion>
    <saml:Issuer>https://idp.example.com/</saml:Issuer>
    <ds:Signature>...</ds:Signature>          ← 서명
    <saml:Subject>
      <saml:NameID Format="...emailAddress">hong@labhub.co.kr</saml:NameID>
      <saml:SubjectConfirmation>...</saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="..." NotOnOrAfter="...">
      <saml:AudienceRestriction>
        <saml:Audience>https://app.example.com/sp</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AttributeStatement>
      <saml:Attribute Name="department"><saml:AttributeValue>개발팀</saml:AttributeValue></saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

SP(我们的系统)要验证的内容与 OIDC 一一对应。

  1. 签名——Response 或 Assertion 是否有有效签名;并确认我们信任的数据确实位于签名覆盖范围内
  2. Issuer——是否为我们登记的 IdP
  3. Audience——是否签发给我们的 SP
  4. NotBefore / NotOnOrAfter——当前是否在有效期内(允许 clock skew)
  5. InResponseTo——是否为我们发送的请求所对应的响应
  6. Destination——是否发送到我们的 ACS URL
  7. 防止重用——同一个 Assertion ID 是否已使用过

第 1 项的第二句话正是 XML Signature Wrapping(XSW)攻击的核心。攻击者把原始 Assertion 藏在文档某处,再把未签名的伪造 Assertion 放到解析器读取的位置。结果是签名验证通过,应用读取的数据却是假的。 因此,不仅要问“签名是否有效”,还要确认“我读取的节点是否就是被签名的节点”。这很难,SAML 不应自行实现,而应使用经过验证的库。

SAML 证书过期——悄然到来的全面故障

SAML 要求 SP 预先登记 IdP 的签名证书。证书过期时,所有用户会同时无法登录。 大型迁移项目中,曾发生因签名证书过期导致 47 分钟全面登录故障的案例。

防御方法很简单:必须把 SAML 签名证书加入证书到期监控列表。多数列表只监控服务器 TLS 证书,因此会漏掉它。

OIDC 与 SAML——实际选择

项目 SAML 2.0 OIDC
数据格式 XML JSON(JWT)
传递路径 主要是前端通道(浏览器 POST) 前端 + 后端通道
移动应用/SPA 不便 适合(PKCE)
元数据交换 XML 元数据文件 discovery 文档(JSON)
大型企业/公共机构 仍有大量使用 新项目逐渐增加
实现难度 高(XML 签名) 中等

新系统优先 OIDC;对方已经使用 SAML,就接入 SAML。 还有一个有趣事实:在 Keycloak 等 IdP 中同时注册 OIDC 客户端与 SAML 客户端后,同一个 SSO 会话里,一边可以得到 JWT,另一边可以得到 XML Assertion。 IdP 相当于协议转换枢纽。在新旧系统并存的 SI 环境中,这一特性是迁移策略的关键。