如何在移动端开发中正确地使用OAuth协议:常见错误剖析

96
登高且赋
0.4 2017.12.22 11:14* 字数 3810

作者在之前的文章中曾经介绍过 OAuth2.0 协议,并将其与OpenID和SAML性对比。然而,在理论上设计协议是一回事,在工程中实现协议是另一回事,由于很多开发人员没有真正理解OAuth2.0的设计意图和技术细节,导致OAuth2.0在很多项目中的实现是有问题,尤其是在移动端的开发中。本文将专注于OAuth2.0在工程实现上常见的问题,帮助读者理解OAuth2.0的技术细节。

OAuth协议的设计初衷,是用于网站间的授权管理,随着其在业界的流行和应用,OAuth也被广泛应用于用户身份的认证。在移动端的火爆之后,OAuth协议也成为移动端的授权和认证的首选。

但是,由于OAuth本身并非为认证而设计,设计之初更没考虑到移动端的使用情况,再加上大部分开发人员对于OAuth协议的流程理解不到位,这就造成了很多项目在协议的实现上有很多地方错误和漏洞。

本文将帮助大家梳理OAuth协议中需要注意的重点,并指出在授权和认证情况下应该如何使用OAuth,同时也澄清一些在移动端中使用OAuth的错误观点。

本文以OAuth 2.0为准进行说明,如果没有特殊说明下文中的OAuth都特指OAuth 2.0版本。关于OAuth协议本身请见作者前文OAuth2.0 协议入门指南OAuth 2.0的四种工作模式中本文只会涉及到常用的默认模式(implicit type)授权码模式(authorization code type)

1. 使用OAuth可能面临的困难

OAuth协议标准很复杂,很少有开发人员能全部熟悉,因为开发人员理解的不到位和协议本身使用场景的改变,造成很多OAuth使用上的困难,简要概括为以下几点:

  • OAuth协议标准中不涉及认证的内容,所以对于认证的使用场景并更没有明确的说明,这导致许多开发者使用OAuth来认证时,常常依赖于自己的直觉,缺乏标准的指导;
  • OAuth 依赖于浏览器的重定向来实现token的传递,但是对于移动端,暂时还没有好的办法来实现同样的效果;
  • OAuth协议针的隐式模式授权码模式的使用场景不尽相同,用于授权和认证时也存在着差异,但是这种差异往往被忽视。
  • OAuth协议规范扩展性极强,很多身份提供商都会自定义OAuth的扩展,但是这些扩展没有统一的标准来引导,甚至还会有错误。

2. OAuth协议流程的关键点

本节将重点介绍在授权和认证两种不同使用场景下,默认模式授权码模式有什么不同,其工作的关键点哪里。

为了便于分析OAuth协议流程,先给出默认模式和授权码的流程,请见下图。

OAuth2.0 implicit
OAuth2.0 authorization code

2.1 用于授权的情况

所谓授权,指的是用户授权依赖方(Relying Party,RP)获取其在服务提供方(Service Provider, SP)保存的个人资源。

授权的安全焦点在于服务提供方,服务提供方要保证用户的个人资源的确是发送给了用户授权的依赖方。

在协议中,RP获得access token代表用户授权,RP通过access token换取资源,但是access token是一种bearer token,也就是说token和持有者的身份并没有绑定,任何人持有该token都能获取用户的资源。因此, 就需要在协议中保证RP的身份为合法的。

2.1.1 默认模式

默认模式通过RP注册时的返回地址Redirection URI来保证RP的身份,在上图默认模式流程的step2中,SP通过RP提供的App ID将RP发来的Redirection URI和其注册时设置的URI进行比对,如果一致才会按照URI中内容重定向返回token,从而来保证token只发给提前注册过的RP。因此默认模式在授权中安全保证的前提是RP提前注册的Redirection URI

2.1.2 授权码模式

授权码模式在默认模式基础上进一步加强,RP在注册时还会分配一个app secret, 其授权过程的安全性保证来源于
step4、step5用授权码换取token的过程中,使用RP与SP共享的app secretredirection URI对RP的身份进行了认证,因为app secret是不能公开的,只有SP和RP共享的秘密,所以可以确保RP身份合法性。在RP身份被验证后,SP再去检验使用授权码换取token的RP是不是当初申请授权码的RP(授权码会与RP的身份绑定)。

2.2 用于认证的情况

所谓认证,即用户向RP证明他/她的确是SP处某个账户身份的持有者,并以此身份登录RP。

认证的安全重点在于RP,需要协议提供如下安全保证:

  1. 从SP发送到RP的身份认证信息不能被第三方篡改;
  2. RP需要确定其获得的身份认证信息的确响应自己的申请;

如通信都是通过HTTPS,可以保证传递的数据不被篡改,但由于OAuth协议并不是为认证而设计的,所以不是所有的模式都可以用于认证的场景。

2.2.1 默认模式

OAuth2.0的默认模式不能用于认证

由于token是一种bearer token,没有和RP的身份绑定,所以默认模式中RP获得的token不能保证就是自己申请的。因此,默认模式用于认证是不安全的。将RP1申请来的token直接发送给RP2,RP2也可以认可该token为用户身份的凭证。

2.2.2 授权码模式

在授权码模式中,SP并不直接将token发给RP,而是先发送授权码,授权码和RP的身份是绑定的,因此RP可以通过授权码来保证自己获得的token就是自己申请的。也就是流程中的step4、step5的授权码换取token的过程保证了token是RP申请的。

同时授权码换取Token是服务器之间的通信,可以直接使用HTTPS,保证信息不被篡改。

3. Web端与移动端使用OAuth的区别

Web端和移动端的环境大不相同,所以为web环境设计的OAuth协议在移动端中使用会面临很多问题。

3.1 缺少重定向机制:

OAuth使用浏览器的重定向(HTTP status 302)在SP和RP之间跳转和传递数据,但是该功能依赖于浏览器,在移动端的app中并没有这样的机制。而更糟糕的是OAuth的标准中认为重定向是技术细节,并没有给出明确的说明。

移动端环境中和重定向最类似的方式是为应用设置自定义URL scheme的机制了(详见Android 跨应用间调用: URL Scheme),其也能实现App间的跳转和传参,但是无法保证接受者的唯一确定性,可能会出现两个应用定义了相同的scheme。

3.2 对应用的身份缺少认证:

浏览器的重定向机制不光保实现了SP和RP之间的跳转,也可以保证token放回到指定的RP处,因为注册的Redirection URI和RP是一一对应的,这样的URI和网站实体的对应是DNS服务器提供。而移动端也没有这样的机制。

3.3 客户端模式带来的逻辑混乱:

在web的情况下,SP和RP都是web网站,但是在移动端的情况,RP可能是native app和服务器的结合。原来RP中需要完成是认证流程,哪部分应该是手机上的app完成,哪些应该是后台的服务器完成,也是在OAuth移动端应用中模糊的地方。

更主要的是,移动端的环境是不可信任的,用户可能在不知情下安装了恶意应用,这些恶意应用可能会窃取用户在移动端中的数据。因此认证或授权中,和安全性相关的的敏感逻辑和数据储存都不能在移动端完成。

4. 移动端开发和实现中存在的问题

正因为存在上面提到的web端和移动端之间的环境差异,OAuth在移动端开发容易出现以下错误。

4.1 将secret存储在应用中

这是一个最常见的错误,如前文所述,OAuth和安全性相关的的敏感逻辑和数据储存都不能在移动端完成。

因此RP的app secret是不能保存在应用中

该错误可能是由于OAuth将其命名为“app secret”,但实际上这个app和移动端的app没有任何关系,其代指的时RP。

secret应该由RP的服务器端保存并SP通信。

4.2 无法充分理解认证与授权的区别

因为没有理解二者的区别,所以应用了OAuth错误的工作模式,使用默认模式来认证用户身份。

Facebook曾经使用OAuth2.0默认模式用于用户登录认证,后来发现了安全问题,但是要修改成授权码模式又和现有系统不兼容,只好才对默认模式进行了自定义化的安全增强,额外消耗了很多人力物力。如果一开始就能清楚知道认证和授权的不同,采用授权码模式来认证用户,就可以避免后面的麻烦事。

4.3在移动设备中处理重定向:

在开发中如何实现类似浏览器重定向的功能在SP和RP之间跳转并传递数据,是实现OAuth功能的关键点。常见的方法有:

  • 使用URI scheme与Intent在应用间跳转;
  • 模拟web端,使用移动端浏览器与WebView;

无论哪种方法都需要保证

  1. 跳转目标的确定性,即保证跳转到的应用真的是发起者要去访问的应用;
  2. 跳转请求来源的真实性,即跳转请求发起者的身份是真实可信的;

4.3.1 使用URI scheme与Intent:

在Android中,由于应用都要经过开发者的秘钥进行签名,所以SP应可以存储所支持的RP的apk的签名信息(Android提供API通过包名查询),在RP进行请求时比对,来确定请求者的身份。这样就可以保证应用之间跳转和传参的目标正确性。

relying_party = Activity.getCallingPackage()
dev_key_hash = getPackageManager().getPackageInfo(relying_party, PackageManager.GET_SIGNATURES);

反之,RP也可以保存SP的apk的签名信息,验证认证结果是否来自认可的SP。

关于IOS的开发,因笔者知识有限,不能具体说明,但是要指出IOS中存在系统漏洞,允许两个应用同时注册相同的URI的,可能会出现scheme劫持,即所谓的“跨应用资源访问(XARA)”,所以最终无法确定传递token的安全。

4.3.4 移动端浏览器与WebView:

由于OAuth本身就是基于浏览器设计的,所以很自然的就想到在移动端也使用浏览器或者是WebView来展示SP以处理OAuth请求。

但是这样做缺少SP的网页和RP的应用之间安全传递信息的途径。上一节提到验证RP的apk签名信息的方法只有在native APP才能使用,SP中的页面无法验证RP身份。同时,RP无法确定SP的页面在哪个应用的WebView运行中,无法断定是否是恶意应用传来的数据。

4.4 修改的OAuth协议容易出现漏洞:

腾讯曾经修改OAuth2.0的默认模式,来用于认证,使用App ID与User ID的hash值保证token和RP身份绑定关系。但是在用户授权的步骤中,没有出现用户确认框,而是默认同意,直接返回token。

这样做存在被攻击的风险:如果认证过程是在恶意的软件中的WebView中实现的,那么恶意的软件就能够拦截并修改App ID与重定向地址,如果没有给用户提示信息(如什么RP应用要求登录)而默认同意,那么攻击者就能通过这种办法以用户的身份访问任意网站。

腾讯后来及时修改了这个问题。

5. 从教训到总结

  1. 意识到认证和授权的不同,使用授权码模式进行认证,避免使用默认模式;
  2. 不要随意自定义扩展OAuth协议来用于认证,最好使用专门为认证而设计协议,比如OpenID Connect
  3. 在进行授权的时候,要保证给予用户展示请求授权的RP,所以应该在授权前提供确认框,其中要包括RP的身份信息;
  4. 我们需要默认用户的设备是不可信的
    1. 开发者不能将任何秘密的信息(如app secret)存储在客户端;
    2. 必须默认所有从设备发送的消息都是可以被篡改的,和服务器的通信要使用HTTPS,应用之间传递参数,也应该使用预先协商的密钥来加密保护;
    3. 开发者必须保证最后收到User ID的RP是用户想要访问的RP;

扩展阅读:

  1. OAuth2.0 协议入门指南
  2. 认证与授权——单点登录协议盘点:OpenID vs OAuth2 vs SAML
  3. OpenID Connect 协议入门指南
  4. Android 跨应用间调用: URL Scheme
单点登录与身份认证