原文链接:http://dearcharles.cn/2017/11/15/OAuth2-0%E5%8D%8F%E8%AE%AE%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3/
OAuth 2.0 是目前比较流行的做法,它率先被Google, Yahoo, Microsoft, Facebook等使用。之所以标注为 2.0,是因为最初有一个1.0协议,但这个1.0协议被弄得太复杂,易用性差,所以没有得到普及。2.0是一个新的设计,协议简单清晰,但它并不兼容1.0,可以说与1.0没什么关系。所以,我就只介绍2.0。
协议的参与者
从引言部分的描述我们可以看出,OAuth的参与实体至少有如下4个:
- RO (resource owner): 资源所有者,对资源具有授权能力的人。如上文中的用户Alice。
- RS (resource server): 资源服务器,它存储资源,并处理对资源的访问请求。如Google资源服务器,它所保管的资源就是用户Alice的照片。
- Client: 第三方应用,它获得RO的授权后便可以去访问RO的资源。如网易印像服务。
- AS (authorization server): 授权服务器,它认证RO的身份,为RO提供授权审批流程,并最终颁发授权令牌(Access Token)。读者请注意,为了便于协议的描述,这里只是在逻辑上把AS与RS区分开来;在物理上,AS与RS的功能可以由同一个服务器来提供服务。
授权类型
在开放授权中,第三方应用(Client)可能是一个Web站点,也可能是在浏览器中运行的一段JavaScript代码,还可能是安装在本地的一个应用程序。这些第三方应用都有各自的安全特性。对于Web站点来说,它与RO浏览器是分离的,它可以自己保存协议中的敏感数据,这些密钥可以不暴露给RO;对于javascript代码和本地安全的应用程序来说,它本来就运行在RO的浏览器中,RO是可以访问到Client在协议中的敏感数据。
OAuth为了支持这些不同类型的第三方应用,提出了多种授权类型,如:
- 授权码 (Authorization Code Grant)
- 隐式授权 (Implicit Grant)
- RO凭证授权 (Resource Owner Password Credentials Grant)
- Client凭证授权 (Client Credentials Grant)
由于本文旨在帮助用户理解OAuth协议,所以我将先介绍这些授权类型的基本思路,然后选择其中最核心、最难理解、也是最广泛使用的一种授权类型——“授权码”,进行深入的介绍。
2.1 授权码模式
(A) web客户端通过将终端用户的user-agent重定向到授权服务器来发起这个流程。客户端传入它的客户端标识符、请求作用域、本地状态和一个重定向URI,在访问被许可(或被拒绝)后授权服务器会重新将终端用户引导回这个URI。
(B) 授权服务器验证终端用户(借助于user-agent),并确定终端用户是否许可客户端的访问请求。
(C) 假定终端用户许可了这次访问,授权服务器会将user-agent重定向到之前提供的重定向URI上去。授权服务器为客户端传回一个授权码做获取访问令牌之用。
(D) 客户端通过验证并传入上一步取得的授权码从授权服务器请求一个访问令牌。(需要带上ClientId和Secret,ClientId和Secret是通过平台授予)
(E) 授权服务器验证客户端私有证书和授权码的有效性并返回访问令牌。
2.2 隐授权模式(implicit grant type)
User-Agent适用于客户端不能保存客户端私有证书的App(纯客户端App,无Server参与)。因为纯客户端的程序不能保存密钥
(A) 客户端将user-agent引导到终端用户授权endpoint。客户端传入它的客户端标识符、请求作用域、本地状态和一个重定向URI,在访问被许可(或被拒绝)后授权服务器会重新将终端用户引导回这个URI。
(B) 授权服务器验证终端用户(通过user-agent)并确认终端用户是许可还是拒绝了客户端的访问请求。
(C) 如果终端用户许可了这次访问,那么授权服务器会将user-agent引导到之前提供的重定向URI。重定向URI会在URI片断{译者注:URI片断是指URI中#号之后的内容}中包含访问令牌。
(D) user-agent响应重定向指令,向web服务器发送不包含URI片断的请求。user-agent在本地保存URI片断。
(E) web服务器返回一个web页面(通常是嵌入了脚本的HTML网页),这个页面能够访问完整的重定向URI,它包含了由user-agent保存的URI片断,同时这个页面能够将包含在URI片断中的访问令牌(和其它参数)提取出来。
(F) user-agent在本地执行由web服务器提供的脚本,该脚本提取出访问令牌并将它传递给客户端。
2.3 密码模式
密码模式就是将密码托管给第三方App,但是必须要保证第三方App高度可信。
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
2.4 客户端模式
这种模式不需要终端用户的参与,只是Client和Server端的交互。通常只用于Client状态的获取。
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
授权码模式的实例化描述
面我以实例化方式来帮助读者理解授权码类型的授权协议的运行过程。假设:
(1) Alice有一个有效的Google帐号;
(2) Facebook.com已经在Google Authorization Server上注册了Client身份,已经获得(client_id, client_secret),注意client_secret是Client与AS之间的一个共享密钥。
(3) Alice想授权Facebook.com查看她的联系人列表(https://www.google.com/m8/feeds)。%E3%80%82)
下图展示了Alice、Facebook.com、Google资源服务器、以及Google OAuth授权服务器之间的协议运行过程。
协议所涉及到的细节都已经在图上了,所以不打算再做详细介绍了。若看懂了此图,OAuth2.0就理解了。
读者请注意,在步骤(4)中,Client需要拿“授权码”去换“授权令牌”时,Client需要向AS证明自己的身份,即证明自己就是步骤(2)中Alice批准授权时的Grantee。这个身份证明的方法主要有两种(图3中使用了第1种):
(1) 通过https直接将client_secret发送给AS,因为client_secret是由Client与AS所共享,所以只要传送client_secret的信道安全即可。
(2) 通过消息认证码来认证Client身份,典型的算法有HMAC-SHA1。在这种方式下,Client无需传送client_secret,只需发送消息请求的signature即可。由于不需要向AS传递敏感数据,所以它只需要使用http即可。
此外, 在步骤(2)中,Google授权服务器需要认证Alice的RO身份,并提供授权界面给Alice进行授权审批。今天Google提供的实例如下图所示,仅供读者理解OAuth这种“现场授权”或”在线授权”的含义。
OAuth2.0授权类型选择
授权模式实际上就是指获取token的方法,选择授权模式主要取决于最终用户使用的客户端的类型,以及你服务对用户的表现形式。
具体的选择方式如下图:
First Party or third party client?
First party指的是,你完全信任这个客户端可以管理好最终用户的认证凭据。比如说对于Spotify的开发者和所有者来说,他们完全信任他们的Spotify iphone客户端。而third party客户端是指我们不信任的客户端。
Access Token Owner?
Access Token表示给一个客户端授权访问某些被保护的资源。如果我们只需要给一个机器授权访问某些资源,而不需要给人授权,那么这些资源应该实现client credential grant。
如果我们需要给人授权访问资源,那么需要视客户端的类型做决定。
Client type?
客户端类型主要是指客户端是否有能力保存秘钥。
如果客户端是个web app,并且有服务器侧的组件,那么我们应该选择authorization code grant
如果客户端是一个纯前端的web app(如:单页面应用),如果客户端可信,我们可以选择password grant,如果客户端不可信可以选择implicit grant
如果客户端是一个本地应用,比如一个手机app,应该选择password grant。
第三方本地应用应该用authorization code grant(通过本地浏览器,不要用嵌入式浏览器,比如iOS强制用户用Safari或者SFSafariViewController,而不用 WKWebView)。
OAuth设计上的安全性考虑
为何引入authorization_code?
协议设计中,为什么要使用authorization_code来交换access_token?这是读者容易想到的一个问题。也就是说,在协议的第3步,为什么不直接将access_token通过重定向方式返回给Client呢?比如:
<figure class="highlight plain" style="display: block; margin: 20px 0px; overflow: auto; padding: 0px; font-size: 13px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border-radius: 1px; font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
|
<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 10px; color: rgb(134, 145, 148); background: rgb(239, 242, 243); line-height: 1.6; border: none; text-align: right;">
1
2
3
4
5
</pre>
|
<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">
HTTP/1.1 302
Location:
</pre>
|
</figure>
如果直接返回access_token,协议将变得更加简洁,而且少一次Client与AS之间的交互,性能也更优。那为何不这么设计呢?协议文档[1]中并没有给出这样设计的理由,但也不难分析:
(1) 浏览器的redirect_uri是一个不安全信道,此方式不适合于传递敏感数据(如access_token)。因为uri可能通过HTTP referrer被传递给其它恶意站点,也可能存在于浏览器cacher或log文件中,这就给攻击者盗取access_token带来了很多机会。另外,此协议也不应该假设RO用户代理的行为是可信赖的,因为RO的浏览器可能早已被攻击者植入了跨站脚本用来监听access_token。因此,access_token通过RO的用户代理传递给Client,会显著扩大access_token被泄露的风险。 但authorization_code可以通过redirect_uri方式来传递,是因为authorization_code并不像access_token一样敏感。即使authorization_code被泄露,攻击者也无法直接拿到access_token,因为拿authorization_code去交换access_token是需要验证Client的真实身份。也就是说,除了Client之外,其他人拿authorization_code是没有用的。 此外,access_token应该只颁发给Client使用,其他任何主体(包括RO)都不应该获取access_token。协议的设计应能保证Client是唯一有能力获取access_token的主体。引入authorization_code之后,便可以保证Client是access_token的唯一持有人。当然,Client也是唯一的有义务需要保护access_token不被泄露。
(2) 引入authorization_code还会带来如下的好处。由于协议需要验证Client的身份,如果不引入authorization_code,这个Client的身份认证只能通过第1步的redirect_uri来传递。同样由于redirect_uri是一个不安全信道,这就额外要求Client必须使用数字签名技术
来进行身份认证,而不能用简单的密码或口令认证方式。引入authorization_code之后,AS可以直接对Client进行身份认证(见步骤4和5),而且可以支持任意的Client认证方式(比如,简单地直接将Client端密钥发送给AS)。
在我们理解了上述安全性考虑之后,读者也许会有豁然开朗的感觉,懂得了引入authorization_code的妙处。那么,是不是一定要引入authorization_code才能解决这些安全问题呢?当然不是。笔者将会在另一篇博文给出一个直接返回access_token的扩展授权类型解决方案,它在满足相同安全性的条件下,使协议更简洁,交互次数更少。
一切只是看上去很美好,但其实很多坑
OAuth2看上去很美好,但是细心观察其实还是有一些漏洞的。
对于授权码和access_token的篡改,在OAuth1中是反复的对Code和Token进行签名,来保证Token不会被篡改,但是OAuth2中却没有,因为OAuth2是基于Https的,所以如果没有Https的支持OAuth2可能还不如OAuth1.
对于redirect_uri的校验,OAuth1中没有提到redirect_uri的校验,那么OAuth2中要求进行redirect_uri的校验。但是如果校验规则过松,也会导致跳转的安全问题。 例如:校验的时候只校验根域名,或者二级域名,但是第三方App对自己的域名保护的不好,导致二级域名被hack那么此时授权码和Token会被窃取。 校验规则不严谨,例如www.baidu.com 但是redirect_uri为:www.a.com.\www.baidu.com,这样授权就被a.com钓走了。
对于CSRF攻击(跨站请求伪造):由于这个授权过程服务器和Client和用户之间有几次交互,但是在得到授权码的时候需要一次回跳,但是这次回跳是可以被阻塞的。
攻击者使用自己的账户申请第三方授权登陆
授权后服务端返回授权码,但是此时组织授权回跳,此时Client并没有接到授权码,也就是阻断了授权流程
攻击者将此跳转链接发给一个正在处于在Client登陆状态的账户
诱骗正常用户点击,那么此时攻击者第三方账户和被攻击账户进行绑定(相当于账户绑定了第三方的账户)
攻击者再次进行第三方授权登陆。这样就劫持了诱骗的账户。转账?删好友?等等就所以搞了。
解决办法:
在进行授权码申请或者是Token申请的时候带上state参数,服务器返回请求时要求携带state参数,在Client处理授权的时候校验此参数。参数可以是当前账户的SessionId,或Cookie的签名串。在Client申请accessToken会验证相关state和当前用户的关系,这样就防止了篡改。
OAuth的校验流程为什么这么复杂,直接授权之后redirect回accessToken不就结了吗?为什么还要返回auth_code之后请求accessToken?
首先,redirect是不安全的,随时可以暂停回调而拿到accessToken,拿到了accessToken也就意味着拿到了授权,但是auth_code是和client相对应的,那么即使拿到了auth_code还需要再次申请accessToken,申请accessToken时需要校验Client和state。同时协议设计的原则就是只有Client能拿到accessToken而用户是拿不到的。