OAuth2.0 面面观
作者介绍—Sheldon Cai
RingCentral Sr Java Engineer,
多年互联网公司一线研发,曾参与大型后端系统的重构与开发,对 Java 技术栈及前沿后端技术有深入研究和丰富的实战经验。
只要是接触过各种开放平台的开发者,对于 OAuth 概念肯定不陌生。但是由于 OAuth 流程比较复杂,对于刚接触的人来说,容易云里雾里。我之前工作上接触 OAuth 比较多,本文以 OAuth2.0 的 RFC 文档为基础,结合自己以前一些工作上的经验,系统地梳理一下 OAuth2.0 规范。
What is OAuth
关于 OAuth 的定义,维基百科是这么说的:
OAuth is an open standard for access delegation, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. This mechanism is used by companies such as Amazon, Google, Facebook, Microsoft and Twitter to permit the users to share information about their accounts with third party applications or websites.
O == Open, Auth == Authorization(授权), not Authentication(认证).
首先要明确的是,OAuth 是一种授权协议,而非认证协议。通过它,用户可以授权第三方应用访问自己保存在资源服务器器上的资源。当然,如果这些资源是账号信息,第三方服务器也可以基于 OAuth 实现类似 SSO 的单点登录,完成登录认证。
OAuth 历史
上面这张图基本涵盖了 OAuth 诞生的相关历史进程。
在 2006 年,Twitter 在开发他们自己的 OpenID 实现,而当时 Ma.gnolia 网站需要一个使用 OpenID 授权访问他们网站资源的方案,双方会面讨论后发现当时并没有一个统一的标准 API 实现这件事。
上面功能的实现者们于 2007 年成立了 OAuth 讨论组,撰写并公布了最早的开放授权(OAuth)草案。这个草案后来得到了 Google 的关注,最终也一起参与了规范的制定。
在 2007 年 10 月,OAuth1.0 草案公布。
在 2008 年 11 月的 IETF 第 73 次会议上,OAuth 得到广泛支持,IETF 正式为它成立了一个工作组。
2010 年,编号为 RFC-5849 的 OAuth1.0 RFC 文档发表。
在 2012 年,OAuth2.0 的 RFC-6749, 和 Bearer Token 的 RFC-6750 相继发表。大多数互联网应用都以此作为授权标准。需要注意的是 OAuth2.0 与 OAuth1.0 并不兼容。
虽然 IETF 的 RFC 意为征求意见稿(Request for Comment),但是经过多年的业界实践,目前它已经是开放授权的事实标准。
本文后续的一些内容,提炼自 IETF 的 RFC 文档,并结合我自己工作中的一些经验总结。
一些概念
了解 OAuth2.0 之前,我们先熟悉几个概念。
角色
OAuth2.0 把整个流程中的参与者分为 4 种角色:
Resource Owner:资源拥有者,通常是我们网站/应用的用户。
Resource Server:资源服务器,Resource Woner 的资源就存储在 Resource Server。比如用户在 Facebook 上存有相册,此时 Facebook 的相册服务器就是 Resource Server.
Client:客户端,一般指第三方应用程序,即资源使用方。比如豆瓣注册时,需要用户的微信头像做豆瓣头像,此时豆瓣就是 Client。
Authorization Server:授权服务器,对 Client 进行授权时验证客户端,用户合法性的节点。Resource Server 和 Authorization Server 可能是同一个(比如资源是账号数据时)也可能不同。
几个术语
首先,Client 想要得到 Authorization Server 的授权,需要先注册。比如各种开放平台,需要先由开发者提供网站地址,应用名称,默认重定向地址等信息,才能为其颁发合法的 Client id 和 Client Secret 进行 OAuth 授权。
Client id:是 Client 在 Authorization Server 注册的标志,格式各家实现不同,但是需要全局唯一。一般注册后不会改变,也有实现方喜欢叫 App id。
Client secret:与 Client id 配对的密钥,格式各家实现不用,保证安全性即可。在进行 OAuth 授权流程时,Client 必须提供 Client id 与 Client secret。如果 Client secret 发生泄露,出于安全考虑,Authorization Server 一般允许注册方重新生成 secret.
User-Agent:一般指用户浏览器,或者 APP。
Access token:是完成授权流程后,Client 得到的票据,访问 Resource Owner 的资源时,需要对其进行验证。认证失败 Authorization Server 将引导 Client 重新进行 OAuth 流程。
Refresh token:类似 AccessToken 的票据,用于刷新 Access token(不需要重新走 OAuth 流程)。Refresh token 是可选项,不一定要实现。
熟悉这些概念后,我们开始介绍 OAuth2.0 定义的标准授权流程。
OAuth2.0 Flow
以下几种 OAuth Flow,摘选自 RFC 相关文档,详情请参考最后引用链接。
为覆盖各种场景,OAuth2.0 划分了 4 种授权流程:
Authorization Code:授权码模式,因为需要在各个节点往返三次,俗称 3 leg。
Implicit:隐式授权,相对于授权码模式做了简化。
Resource Owner Password Credentials:密码认证模式。
Client Credentials:客户端认证模式。
下面详细介绍这几种模式。
Authorization Code Grant
下图描述了一个完整的 Authorization Code 模式授权流程,Client 与其他角色的交互通过 User-Agent,这里 Client 包含前端和后端服务器。
步骤 A:用户在通过 User-Agent(浏览器)使用 Client 时,Client 需要访问用户 Resource Owner 的资源,此时发起了 OAuth 流程。Client 携带客户端认证信息(Client id)、请求资源的范围、本地状态,重定向地址等重定向到 Authorization Server,用户看到授权确认页面。
步骤 B:用户认证并确认授权信息,Authorization Server 判断用户是否合法来进行下一步授权或者返回错误。
步骤 C:如果用户合法且同意授权,Authorization Server 使用第一步 Client 提交的重定向地址重定向浏览器,并携带授权码和之前 Client 提供的本地状态信息。
步骤 D:Client 使用授权码找 Authorization Server 交换 access token(出于安全性考虑,一般由 Client 的服务端发起),为了严格验证,这一步除了携带授权码,还需要前面使用的重定向地址。
步骤 E:Authorization Server 验证 Client 提交的授权码是否有效,重定向地址是否与步骤 C 匹配。如果验证通过,将返回 access token 和 refresh token(可选)给 Client。
得到 access token 后,Client 可以在 token 失效前,访问 Resource Server 得到已授权的用户资源。OAuth2.0 在 Client 与 Resource Server 之间,设置了一个授权层(authorization layer),Client 通过得到的授权令牌访问资源,对于资源访问权限、时效在颁发令牌时控制。
流程中几个步骤涉及到的接口:
重定向授权页(步骤 A)
请求例子:
参数说明:
重定向回 Client(步骤 C)
请求例子:
参数说明:
从 Authorization Server 获取 token(步骤 D)
请求例子:
参数说明:
Authorization Server 返回 token(步骤 E)
响应结果例子:
参数说明:
Implicit Grant
Implicit 授权的流程如下图,与 Authorization Code 相比,少了返回授权码这一步,Authorization Server 直接返回 token 至 Client 的前端,Client 方面没有后端参与。图中的 Web-Hosted Client Resource 可以认为是 Client 的前端资源容器,比如前端服务器,APP 等。
步骤 A:与 Authorization Code 流程类似,Client 携带客户端认证信息(Client id 和 Secret)、请求资源的范围、本地状态,重定向地址等重定向到 Authorization Server,用户看到授权确认页面。
步骤 B:用户认证并确认授权信息,Authorization Server 判断用户是否合法来进行下一步授权或者返回错误。
步骤 C:如果用户合法且同意授权,Authorization Server 使用第一步 Client 提交的重定向地址重定向浏览器,并将 token 携带在 URI Fragment 中一并返回。
步骤 D:User-Agent 顺着重定向指示向 Web-Hosted Client Resource 发起请求(按 RFC2616 该请求不包含 Fragment)。User-Agent 在本地保留 Fragment 信息。
步骤 E:Web-Hosted Client Resource 返回一个网页(通常是带有嵌入式脚本的 HTML),该网页能够提取 URI 中的 Fragment 和其他参数。
步骤 F:在 User-Agent 中使用上一步提供的脚本提取 URL 中的 token。
步骤 G:User-Agent 传送 token 给 Client。
Implicit 比起 Authorization Code 来说,少了 Client 使用授权码换 Token 的过程,而是直接把 token 提供给 User-Agent 让 Client 提取。整个流程中使用 URL 传递 token,不需要 Client 的服务端参与,且没有严格验证 Client 信息,安全性欠佳。使用这个方式授权,需要在安全性和便利性之间做好权衡。
流程中几个步骤涉及到的接口:
重定向授权页(步骤 A)
请求例子:
参数说明:
携带 token 重定向回 Client(步骤 C)
请求例子:
参数说明:
Implicit Grant 不严格验证 Client,因此这里不提供 refresh_token(以防 Client 不经用户同意,使用 refresh_token 不断得到授权)。同时 Implicit Grant 的 access_token 是通过 url 的 hash 返回的,不会在网络上传输,但是还是存在泄漏的可能(如 User-Agent 本身不安全)。
Resource Owner Password Credentials Grant
这种授权方式其实是常见的用户名密码认证方式。使用这种授权的 Client 必须是高度可信的,比如操作系统。只有当其他的流程不能使用时,才启用这种方式,同时 Authorization Server 必须特别关注 Client 确保不会出现安全问题。整个过程中,Client 不得保存用户的密码(只能由 Client 来保证,所以 Client 必须是高度可信的)。
步骤 A:resource owner 提供给 Client 用户名密码。
步骤 B:Client 直接使用用户名密码向 Authorization Server 进行认证,并请求 token。
步骤 C:Authorization Server 认证 Client 信息和用户名密码,验证通过后返回 token。
流程中几个步骤涉及到的接口:
Client 提交用户名密码请求 token(步骤 B)
请求例子:
参数说明:
Authorization Server 返回 token 信息(步骤 C)
响应例子:
这里的响应参数跟 Authorization Code 模式是一样的。
Client Credentials Grant
该模式是 Client 访问实现与 Authorization Server 约定好的资源。Client 以自己的名义,而不是以用户的名义,向 Authorization Server 进行认证。严格地说,Client Credentials 模式并不属于 OAuth 框架所要解决的问题。在这种模式中,用户直接向 Client 注册,Client 以自己的名义要求 Authorization Server 提供服务,其实不存在授权问题。
步骤 A:Client 向 Authorization Server 进行身份认证,并请求 token。
步骤 B:Authorization Server 对 Client 信息进行认证,有效则发放 token。
流程中几个步骤涉及到的接口:
Client 申请 token(步骤 A)
请求例子:
参数说明:
这一步 Authorization Server 必须验证 Client。
Authorization Server 返回 token 信息(步骤 B)
响应例子:
这里的响应参数跟 Authorization Code 模式也是一样的。
PKCE(Proof Key for Code Exchange)
随着无服务端移动应用或 SPA 的流行,IETF 针对 Implicit 授权提出了优化方案,在 RFC-6749 的四种 Flow 之外另外定义了一种更安全的 PKCE 模式(RFC-7636)。PKCE 的流程大概如下:
这里引入了几个新的变量:t_m(摘要算法),code_verifier,code_challenge(即图中经过算法 t_m 计算后得到的 t(code_verifier)参数)
Client 随机生成一串字符并作 URL-Safe 的 Base64 编码处理, 结果用作 code_verifier。
将这串字符通过 SHA256 哈希,并用 URL-Safe 的 Base64 编码处理,结果用作 code_challenge。
Client 使用把 code_challenge,请求 Authorization Server,获取 Authorization Code。(步骤 A)
Authorization Server 认证成功后,返回 Authorization Code(步骤 B)。
Client 把 Authorization Code 和 code_verifier 请求 Authorization Server,换取 Access Token。
Authorization Server 返回 token。(步骤 D)
由于中间人不能由 code_challenge 逆推 code_verifier,因此即使中间人截获了 code_challenge, Authorization Code 等,也无法换取 Access Token, 避免了 implicit 模式的安全问题。
流程中几个步骤涉及到的接口:
Client 重定向授权页(步骤 A)
请求例子:
response_type,client_id,redirect_uri,scope,state 跟 implicit 模式是一样的。重点看下其他几个参数。
参数说明:
Authorization Server 返回 token 信息(步骤 B)
响应例子:
这里的响应参数跟 Authorization Code 模式也是一样的。
Token
对于 token(Access Token 和 Refresh Token)需要使用什么样的格式,其实没有硬性要求,不同平台有不同的实现方式。这里列举两种常见的 token 规范,Bearer Token 和 JWT。
Bearer Token
OAuth 诞生时就已经定义了两种 token 格式:Bearer Token 和 Mac Token,Mac 主要使用在无 https 的环境下,由于 OAuth2.0 已经要求所有参与者必须使用 HTTPS,所以 Mac 格式不在我们今天讨论范围。Bearer Token 由 RFC-6750 定义。
Bearer Token 格式用 BNF 范式表示就是:
换成程序员比较容易理解的正则表达式就是:
所以所谓的 Bearer Token 就是以数字、大小写字母、破折号、小数点、下划线、波浪线、加号、正斜杠、等号结尾组成的 Base64 编码字符串。在 HTTP 传输过程中,需要以'Bearer '作为前缀标识。
Bearer Token 的三种传输方式
RFC-6750 定义了三种传输 Bearer Token 的方式,优先级依次递减:
Authorization Request Header Field(使用 HTTP Header 的 Authorization 字段传递)
Form-Encoded Body Parameter(使用表单参数传递)
URI Query Parameter(使用 URI 参数传递)
由于 Cookie 容易被 CSRF 攻击,不建议采用 cookie 的方式传输 token。尽量不要用 URI 参数的方法,因为浏览器历史记录、服务器日志等可能泄露 URI 上的机密信息。
JWT
JWT(JSON Web Token)是近几年移动端常用的 token,它可以直接将一些信息编码传递,对客户端更友好。使用 JWT 有以下有点:
验证 token 不需要另外的缓存或者数据库,通过约定好的加密方式解密就行。
因为 json 的通用性,所以 JWT 是可以进行跨语言支持的,像 JAVA,JavaScript,NodeJS,PHP 等很多语言都可以使用。
因为有了 payload 部分,所以 JWT 可以在自身存储一些其他业务逻辑所必要的非敏感信息。便于传输,jwt 的构成非常简单,字节占用很小,所以它是非常便于传输的。
它不需要在服务端保存会话信息, 所以它易于应用的扩展。
使用 JWT 也必须注意一些问题:
不应该在 jwt 的 payload 部分存放敏感信息,因为该部分是客户端可解密的部分。
保护好 secret 私钥,该私钥非常重要。
如果可以,请使用 https 协议传递 JWT。
JWT 也有自己的 RFC 规范 RFC-7519,这里简单介绍一下它的格式。详细请参考文末的 RFC 链接。
JWT 的格式很简单,一个 JWT 字符串分为 Header,Payload,Signature 三部分,他们的原始字符串经过编码后由小数点分隔连接起来。
Header 记录着 token 类型和摘要算法,这里的明文最后要经过 Base64URL 编码:
Payload 记录着业务信息和用户数据(非敏感),字段可以根据需求自定义,出于安全性考虑,实现方会再加上 expire 过期时间字段控制生命周期。这里的明文同样也要经过 Base64URL 编码:
Signature 是 Header 和 Payload 经过摘要算法处理后的签名信息,使用的摘要算法需要同 Header 中 alg 属性一致,这里是 HS256。secret 是加密需要的密钥,使用对称加密算法的话密钥泄漏影响较大。如果使用非对称加密算法(如 RSA256),使用的是公钥验证签名,风险就小很多:
连接编码后的三个部分,就得到一个 JWT 字符串:
所以当 Server 端颁发 JWT 后,Client 就可以根据约定好的 secret,摘要算法验证 Signature 并提取 Payload 信息。
OAuth 面临的安全问题
OAuth2.0 作为一个授权协议,安全问题尤为重要。OAuth 大规模应用的这些年来,主要的安全问题可以分为以下几类:
Client Authentication(客户端错误认证),作为 Client 的开发者,必须保护好自己的 client_id client_secret,谨防盗用。
Code or Token Steal(票据窃取),OAuth 是票据协议,无法区分使用票据的人是否合法。所以作为 Authorization Server,必须对 token 的失效机制做好控制(如合理的失效时间,限制 Code 只能用一次,允许用户管理自己已授权的 token)。作为 Client,必须确保用户授权的 token 不被采集(最常见的问题就是在 log 中记录 access token)
Cross-Site Request Forgery(CSRF 攻击),Authorization Code Grant 模式流程较长,存在 CSRF 隐患。
Authorization Code Redirection URI Manipulation(重定向地址篡改),重定向地址篡改是钓鱼网站常用的攻击手段。
前面两种是任何认证授权系统都需要考虑的安全问题,这里重点介绍下后面两种跟 OAuth 流程比较相关的安全问题。
CSRF 攻击
在 OAuth2.0 流程中实施 CSRF 攻击的流程如下:
原理
攻击者预先准备好使用自己账号授权生成的 authorization code 的回调地址,引诱用户点击。
用户点击后,变成使用攻击者的账号完成 Oauth 流程得到 token。
在的第三方 app 绑定账号的场景,攻击者就可以使用自己的账号完成 OAuth 登陆用户的第三方 app。
防范措施
要防止这样的攻击其实很容易,使用 RFC 规范中推荐的 state 参数即可,但是由于增加了开发工作量,很多开发者使用 OAuth2.0 时,经常忽略这个参数。具体细节如下:
在 Authorization Code Grant 或者 implicit Grant 流程的第一步,调用/authorize 接口时,带上 state 参数,state 的值由 Client 指定,生成规则需保证足够随机又有一定业务含义,他人无法轻易假冒。
Client 需要保存 state 参数。
在 Authorization Server 认证成功重定向回 Client 时,会将 state 原样带回,此时 Client 需要验证 state 参数是否一致。
Authorization Code 流程重定向地址篡改
对重定向地址检查也是一个时常被忽略的安全弱点。
原理
对于一个正常的第三方 Client 应用 A,攻击者自己也作为一个 Client,伪造一个应用 A 的/authorize 请求的链接,其中 redirect_uri 指向的是攻击者的 Client。
攻击者诱导用户点击伪造的链接,发起 OAuth2.0 的 Authorization Code Grant 流程。
用户完成认证后,Authorization Server 携带 Code 重定向回攻击者 Client。
攻击者准备一个自己的 Code,将上一步应用 A 的 Code 替换,伪造一条应用 A 的回调请求返回给应用 A。(此时 Code 被替换成了攻击者的 Code)
应用 A 的 Client 在不清楚 Code 被替换的情况下,继续完成 Authorization Code Grant 流程,使用攻击者的 Code 换取 Access Token。
此时用户走完 OAuth 流程,但是在应用 A 上得到的却是攻击者帐号的授权。大家会觉得,这样有什么问题,又不是用户的授权泄漏。这种攻击方式可以针对绑定帐号的场景,比如用户本来要将豆瓣帐号与微博帐号关联,使用微博的 OAuth 授权来登陆豆瓣。而被这样钓鱼以后,自己的豆瓣帐号绑定的是攻击者的微博帐号,此时攻击者就可以用他的微博帐号登陆用户的豆瓣帐号了。
防范方法
Client 注册时,需要开发者提供域名与 Client 绑定。
Authorization Server 对/authorize 接口验证的 redirect_uri 参数验证,确认与 Client 注册时提供的域名一致。
Authorization Server 对/access_token 接口的 redirect_uri 参数进行验证,保证与 Client 发起 /authorize 请求时的 redirect_uri 一致。
对于 Authorization Server 的 Code 换 token 接口,可以要求 Client 提供 client_id 和 secret,校验此时的 code 是否产生自同一个 client_id。
对于上面提到的 CSRF 和钓鱼攻击,Client 方面如果增加一些授权成功后的提示给用户(比如平台成功与 xxx 帐号绑定),可以避免用户无意识地授权的情况发生。上面的例子只是简单展示了 OAuth 授权中需要开发者关注的安全细节,关于 OAuth 安全想要了解更多,可以参考文末的 OAuth 安全指南。
小结
OAuth2.0 规范将参与者划分为 Resource Owner,Resource Server,Client,Authorization Server 四种角色。
RFC-6749 定义了四种 OAuth2.0 Grant Flow:Authorization Code Grant,Implicit Grant,Resource Owner Password Credentials Grant,Client Credentials Grant。其中前两种是比较常用的 OAuth2.0 授权模式。
对于移动端 APP 或者 SPA 应用,可以考虑使用 PKCE 模式减少 Implicit Grant 的安全风险。
对于 Token 的格式,建议使用 Bearer Token 或者 JWT。
由于 OAuth2.0 的 Flow 步骤较长,不管是 Client 端还是 Authorization Server 端,在使用 OAuth2.0 的时候,最好严格按照 RFC 规范执行,可以最大程度地减少安全隐患。同时也要注意业界关于 OAuth 漏洞的披露,及时修复漏洞。
参考链接
OAuth2.0: https://oauth.net/2/
OAuth2.0(RFC-6749): https://tools.ietf.org/html/rfc6749
PKCE(RFC-7636): https://tools.ietf.org/html/rfc7636
Bearer token(RFC-6750): https://tools.ietf.org/html/rfc6750
JWT(RFC-7519): https://tools.ietf.org/html/rfc7519
乌云平台(备份)OAuth 安全指南: http://drops.xmd5.com/static/drops/papers-1989.html
版权声明: 本文为 InfoQ 作者【RingCentral铃盛】的原创文章。
原文链接:【http://xie.infoq.cn/article/41ac7535056b5000f8e3870cf】。未经作者许可,禁止转载。
评论