企业账号安全体系建设之单点登录系统设计

引言

单点登录(SSO,Single Sign On)是一种身份验证解决方案,可让用户通过一次性用户身份验证登录多个相互信任的应用程序和网站。

SSO能解决的问题:

  • 提高用户体验:用户不必在每个系统中都进行注册、登录
  • 减轻开发人员的负担:开发人员不需要为每个系统都设计一个单独的账号和登录系统,并且很难几乎不可能要求所有开发人员保证这些系统都达到合格的安全水位以上
  • 改善安全状况:通过收敛用户的登录入口,可以审计用户的登录行为

SSO有以下几种协议来对用户进行身份验证:

  • SAML(Security Assertion Markup Language, 安全断言标记语言):SAML 使用 XML 来交换用户标识数据,基于 SAML 的 SSO 服务提供更好的安全性和灵活性,因为应用程序不需要在其系统上存储用户凭证。因为该协议不在本文介绍的重点之内,详情参考[7]
  • OIDC(OpenID Connect,):是使用一组用户凭证访问多个站点的方法,譬如使用微信账号登录其他网站的场景,就是这种开放标准的应用。
  • Kerberos:一种基于票证的身份验证系统,可让两方或多方在网络上相互验证其身份。它使用安全密码学来防止未经授权访问在服务器、客户端和密钥分发中心之间传输的标识信息。所谓的票证,可以理解为多个请求体参数,这些参数主要包括客户端的Name,IP,需要访问的网络服务的地址Server IP,ST的有效时间,时间戳以及用于客户端和服务端之间通信的Session Key(只在一次Session会话中起作用,即使密钥被劫持,等到密钥被破解可能这次会话都早已结束)

Kerberos协议

感慨于kerberos协议的精妙,所以研究了一下整个认证过程,概括如下[2]

kerberos协议认证过程

  1. 认证服务器首先在数据库里存储客户端和服务端的身份信息(譬如ip,用户名)和双方的密钥(该密钥通过密码生成)
  2. 客户端首先与认证服务器连接,主要是验证客户端是否已经在数据库里(用户名和ip),认证通过后返回一个被加密的票据授予票据(主要包含了客户端的Name,IP,当前时间戳等),客户端用自己的密钥解密部份票据内容后会根据时间戳判断该时间戳与自己发送请求时的时间之间的差值是否大于5分钟,如果大于五分钟则认为该认证服务器是伪造的,认证至此失败;另一部份票据内容是用票据授予服务器的密钥加密的,里面保存了客户端的身份信息,客户端无法解密。
  3. 客户端一次性将三种内容发送给票据授予服务器,包括:服务端IP,自身的身份信息,第2步里无法解密的部份票据授予票据。票据授予服务器首先验证服务端IP是否在数据库内,再判断时延,最后票据授予服务器用自己的密钥解开票据授予票据获取到认证服务器封装好的客户端信息,将该信息与客户端发送过来的自身身份信息比较。一致后就认为通过了验证并返回用服务端密钥加密的票证。
  4. 客户端用服务端的密钥加密自身的身份信息,与第三步获得的票据一起发送给服务端。服务端解密后验证身份信息是否与票据里的身份信息一致,然后将接受请求的响应加密后返回。客户端解密后会验证服务端身份,至此双方完成了身份验证。

总结,核心步骤:

  1. 验证client与server的ip是否在数据库中
  2. 验证时间戳是否超出时延
  3. AS用TGS的密钥加密client的身份信息,让TGS解密后验证是否一致
  4. TGS用server的密钥加密client的身份信息,让server解密后验证是否一致

OIDC协议

OIDC 是基于 OAuth 2.0 构建的身份认证框架协议,但是 OIDC 与 OAuth2.0 有概念的区别:

  • OAuth2.0 是一种授权协议,主要用于资源授权(只做一件事)
  • OIDC 是 OAuth 2.0 协议的超集,能够认证用户并完成资源授权(做了两件事)

在介绍OIDC之前,先介绍OAuth协议,OAuth 1.0 现在已经是废弃状态,不需要研究。OAuth 2.0 的授权流程如下:

OAuth2.0 授权流程

  1. 第三方应用要求用户登录,用户选择了使用微信帐号登录
  2. 第三方应用拉起微信APP,微信APP会弹出一个登录确认框
  3. 用户点击确认按钮后微信会给微信服务平台发送一个请求
  4. 微信服务平台给微信返回一个授权码,然后微信重新拉起第三方应用并附带授权码
  5. 第三方应用APP会将授权码传递给服务端,由服务端向微信的服务平台发起请求,并附带上授权码,app_id和app_secret(这两者都是第三方应用在微信服务平台上注册时生成的)
  6. 微信服务平台返回一个access_token,如果需要,还会返回 refresh_token。
  7. 第三方应用使用这个access_token访问微信服务平台,微信服务平台验证了access_token之后就返回用户的微信帐号信息

可以看到,整个过程中唯一可能让用户输入帐号密码的地方就是第二步拉起微信APP时,如果你此时在微信退出了登录就会让你输入账号密码来认证,但这个认证过程不在 OAuth2.0 协议的流程里,因此 OAuth2.0 并不是身份认证协议。

问题1: 为什么第四步要引入授权码,没有授权码环节可以吗?
如果没有了授权码,那么第四步就变成了微信服务平台直接给第三方应用服务端返回 access_token,注意,不能将 access_token 返回给第三方应用APP的客户端,这样会导致 access_token 存在失窃的安全风险,然后第三方应用服务端就直接拿着 access_token 去拿数据。此时,原本第四步里微信会重新拉起第三方应用的这个步骤就消失了,意味着第三方应用实际上已经登录完成了,但你仍然停留在微信的界面上并且没人通知你登录成功,想想就知道这个用户体验就很差。所以授权码的作用就是为了重新建立起第三方应用的一次连接,但又不能让访问令牌暴露出去。授权码就是一个临时的、间接的凭证,并且可以直接返回给第三方应用APP,甚至直接暴露在网络上也没问题,而 access_token 是安全保密性要求极高的令牌,只能在服务端之间用https传输。

问题2: 如果窃取了授权码,是否也可以获取到 access_token?
需要同步窃取到第三方应用的app_id和app_secret,并且在授权码尚未过期的时间内可以。

问题3: 不使用授权码,在第四步让微信重新拉起第三方应用并用https传递 access_token 是否可以?
https是用来保证传输安全的,access_token 到达了第三方应用APP端后会遭遇怎样的风险是未知的,可能移动端本身已经被hack了,黑客已经拿到了https的证书,此时就可以直接解开并获得 access_token 了。所以传输过程安全了,并不代表存储就是安全的。


看完了 OAuth2.0 协议之后,再看 OIDC 协议的流程:

OIDC协议工作时许图

可以看到,用 OAuth 2.0 实现 OIDC 的最关键的方法是在原有 OAuth 2.0 流程的基础上增加 ID 令牌(id_token)和 UserInfo 端点(可以理解为认证平台的一个接口,用于给第三方应用获取用户信息,譬如: /oauth2/userInfo)。id_token是一个jwt格式的令牌,得益于jwt的自包含性,紧凑性以及防篡改机制,使得 id_token 可以安全的传递给第三方客户端程序并且容易被验证。第三方软件可以通过解析 id_token 获取关键用户标识信息(其实就是user_id,如果第三方系统不需要获取更详细的用户信息,就不需要房屋UserInfo端点)来记录用户状态,然后通过 Userinfo 端点来获取更详细的用户信息。有了用户态和用户信息,也就理所当然地实现了一个身份认证

ID Token的主要构成部分如下(使用OAuth2流程的OIDC)[4]

  1. iss = Issuer Identifier:必须。提供认证信息者的唯一标识。一般是一个https的url(不包含querystring和fragment部分)。
  2. sub = Subject Identifier:必须。iss提供的EU的标识,在iss范围内唯一。它会被RP用来标识唯一的用户。最长为255个ASCII个字符。
  3. aud = Audience(s):必须。标识ID Token的受众。必须包含OAuth2的client_id
  4. exp = Expiration time:必须。过期时间,超过此时间的ID Token会作废不再被验证通过。
  5. iat = Issued At Time:必须。JWT的构建的时间。
  6. auth_time = AuthenticationTime:EU完成认证的时间。如果RP发送AuthN请求的时候携带max_age的参数,则此Claim是必须的。
  7. nonce:RP发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID Token和RP本身的Session信息。
  8. acr = Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。
  9. amr = Authentication Methods References:可选。表示一组认证方法。
  10. azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用。

典型的例子:

{
"iss": "https://server.example.com",
"sub": "24400320",
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969,
"acr": "urn:mace:incommon:iap:silver"
}

OIDC的AuthN请求中scope参数必须要有一个值为的openid的参数(后面会详细介绍AuthN请求所需的参数),用来区分这是一个OIDC的Authentication请求,而不是OAuth2的Authorization请求。

基于 OIDC 协议实现的 SSO 系统

假设一个第三方软件有三个子应用,对应的域名分别是 a1.com、a2.com、a3.com,现在要设计一个SSO系统让用户登录了 a1.com 之后也能顺利登录其他两个域名,这就是SSO(单点登录:一次登录,畅通所有)

基于OIDC实现的SSO

当用户在 a1.com 登录成功后,OP会为该用户创建 session,用户在登录a2.com或a3.com时会在 cookie 中携带 session_id,从而避免了反复输入密码。

Q1:用 access_token 也可以拿到用户信息,为什么还要引入 id_token ?[1]
access_token 永远不能被任何第三方软件去解析,就是一个令牌,用来后续请求受保护资源;而 id_token 是可以直接被第三方软件解析的。而且,这两种令牌还具有不同的生命周期,id_token 通常会很快过期,而 access_token 可以在用户离开后的很长时间内用于获取受保护资源。

引用

[1] OAuth 2.0 实战
[2] 详解kerberos认证原理
[3] Microsoft identity platform and OAuth 2.0 authorization code flow
[4] [认证 & 授权] 4. OIDC(OpenId Connect)身份认证(核心部分)
[5] OpenID Connect Core 1.0 incorporating errata set 1
[6] OpenID Connect - Google
[7] SAML

文章作者: kylinlin
文章链接: https://kylinlingh.github.io/2022/10/17/%E4%BC%81%E4%B8%9A%E8%B4%A6%E5%8F%B7%E5%AE%89%E5%85%A8%E4%BD%93%E7%B3%BB%E5%BB%BA%E8%AE%BE%E4%B9%8B%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Water&Melon's Blog