OAuth 2.0 及 API 授权


现代应用多是围绕 API 设计的。API 可以使应用程序重用 service 的逻辑。API 提供了数据与服务的访问接口,所以应用程序在授权后才能调用 API。过去,用户通常不得不与应用共享其凭证来授权应用程序访问 API,但是这通常会给应用过大的权限,不利于用户信息的保护。本文讲述 OAuth 2.0 协议解决 API 授权的问题。

1. API 授权

应用程序通过访问 API 代表用户获取用户数据,或者代表应用本身访问其自身拥有的内容,如下图所示:

图 1:API 授权:user based vs. Client based Flow

在上图中,WriteAPaper.com 应用是一个帮用户撰写论文的编辑器,它调用 2 个 API,2 个 API 属于不同的组织。第一个 API 是 famousquote.com,它提供了论文中用到的引用。第二个 API 在 documents.com,它提供文档存储服务。这里还有一个手机端应用调用 documents.com 的 API 来从用户手机端为用户提供文档访问服务。

WriteAPaper 应用调用位于 famousquote.com 的 API 时,它代表应用自身,因为这些引用不属于用户拥有的数据。应用只需要注册一个授权的客户端来访问这些 API。但当程序访问 documents.com 的时候,必须代理用户菜呢个请求用户文档,因此需要获取用户授权。手机端程序需要用户数据的读取权限。

2. OAuth 2.0

OAuth 2.0 授权框架于 2012 年发布,它提供了授权应用访问第三方 API 的解决方案,无需用户提供自己的凭证。

其主要的使用场景涉及一个用户(也被称为“资源所有者”,resource owner),用户想要允许应用访问一些位于资源服务器(resource server)的受保护的资源。以图 1 为例,用户将文档存储在资源服务器 documents.com,用户使用 WriteAPaper 应用基于上传到 documents.com 的文档撰写论文。用户自然希望授权 WriteAPaper 访问资源服务器的资源。

在 OAuth 2.0 出现之前,通常的解决方案都有一定风险。用户不得不将自己在 documents.com 的 credential 给 WriteAPaper。一旦 WriteAPaper 得到用户凭证,它就可以得到用户账户的全部权限。这风险太大了!如果 WriteAPaper 以不安全的方式存储用户凭证,那么如果 WriteAPaper 被攻陷,用户在 documents.com 的账户就会全部泄露。另外,用户也无法撤销对 WriteAPaper 的授权,除非更改密码,可是这样的话其它曾经被授权的应用也是去访问权限了。

图2 Without OAuth 2.0

OAuth 2.0 提供了一种更好的解决方案。它允许用户在无需给应用程序自己 credential 的前提下授权应用访问 server API,并可以在某种程度上限制应用程序可以执行的操作。使用 OAuth 2.0,当应用程序需要调用 API 时,先向 authorization server 发送一个访问 API 的授权请求。Authorization server 处理这些访问请求并返回一个可以用来访问 API 的安全令牌(security token)。应用程序向授权服务器发送请求时,需要指明 scope。授权服务器对其进行评估,如果决定授权,就返回一个 token 给应用程序。

如果应用程序请求访问用户数据,授权服务器会对用户进行身份验证,并询问用户是否同意授权。身份验证步骤确保同意授权的确实是资源的所有者。如果用户同意授权,那么应用程序会得到一个 token 以代表用户访问 API。这个 token 就是 access token,它允许应用程序在用户授权的范围内发送 API 请求。这种方式无需用户为应用程序提供自己在 server 的凭证。下图展示了这种授权方式的流程:

图 3 With OAuth 2.0

值得强调的是,OAuth 2.0 提供的是授权解决方案,而不是身份验证方案。OAuth 2.0 的 token 只用于 API 访问,不用于传递身份信息。

3. 术语

为了详细描述 OAuth 2.0 协议,需要定义一些术语(terminology)。

Roles

OAuth 2.0 定义了 4 中角色:

  • Resource Server 资源服务器:保存需要被应用程序(client)访问的受保护资源的服务端(with API)
  • Resource Owner 资源所有者:在资源服务器上拥有受保护资源的用户或实体
  • Client 客户端:需要访问资源服务器上受保护资源的应用程序。
  • Authorization Server 授权服务器:被资源服务器信任的服务,用来为 client 授权。它验证应用程序或资源所有者,如果应用程序代表资源所有者发出请求,则请求资源所有者的同意。在 OAuth 2.0 中,资源服务器是依赖授权服务器的。授权服务器和资源服务器可以由同一实体维护。

Confidential VS. Public Clients 机密客户端与公共客户端

OAuth 2.0 定义了 2 种类型的客户端:

  • Confidential Client :运行在受保护的服务器上的应用程序,可以安全地存储机密信息以便向授权服务器进行身份验证。
  • Public Client:主要运行在客户端设备(本机原生应用或浏览器应用)的应用程序,不能安全存储私密信息

Client Profiles

OAuth 2.0 根据应用程序的拓扑结构定义了 3 种 profile(配置):

  • Web Application:代码运行在受保护的后端服务器上。服务器可以安全地存储验证客户端所需的 secret,以及从授权服务器接收的 token。
  • User Agent-Based App:可以是代码运行在用户浏览器中的公共客户端。比如,在浏览器中运行的基于 JavaScript 的单页程序。
  • Native Application:可以是在用户设备上运行的公共客户端,比如一个移动应用程序或桌面应用程序。

在实际中,这些定义可能会发生重叠。因为一个web应用程序可能提供包含一些JavaScript的HTML页面,而单页面应用程序可能有一个较小的后端。

Token 和 Authorization code 令牌和授权码

OAuth 定义了 2 种安全令牌(security token)和 1 种授权码(authorization code)。

  • Authorization Code:是返回给应用程序的一个中间的、不透明的码,被用来获取 access token 和/或 refresh token。每个授权码只能使用一次。
  • Access Token:应用程序用来访问 API 的令牌,令牌有过期时间。
  • Refresh Token:当 access token 过期时,应用程序可以通过 refresh token 获得一个新的 access token。

4. OAuth 2.0 如何工作?

OAuth 2.0 为应用程序获得 API 调用授权提供了 4 种方式:

  • 授权码(authorization-code)
  • 隐藏式(implicit)
  • 密码式(password):
  • 客户端凭证(client credentials)

4.1 授权码授权

授权码授权通过 2 次对授权服务器的请求得到 access token。

在第一次请求中,用户浏览器被重定向到授权服务器的授权端点(authorization endpoint),代表用户请求 API 访问授权。浏览器重定向允许授权服务器与用户交互,对用户进行身份验证,并获取用户对授权请的同意。获得用户同意后,授权服务器返回授权码并将用户浏览器重定向回应用程序。应用程序使用授权码向授权服务器的 token endpoint 发送第二次请求(后端请求),以获取 access token。授权服务器返回 access token 给应用程序,应用程序使用 access token 访问 API。

图 4 授权码授权

  1. 用户(资源所有者)访问应用程序
  2. 应用程序将授权请求重定向到授权服务器的授权端点(authorization endpoint)
  3. 授权服务器提示用户进行身份验证,询问是否同意授权
  4. 用户认证并同意授权请求
  5. 授权服务器将用户浏览器重定向到应用程序提供的回调 URL 并返回授权码
  6. 应用程序使用授权码请求授权服务器的 token endpoint
  7. 授权服务器返回 access token (以及一个可选的 refresh token)
  8. 应用程序使用 access token 访问 API

值得注意的是,第二个请求可以由应用程序的后端直接发送到授权服务器的 token endpoint。这使得应用程序后端(假定其能够安全地管理一个 authentication secret)使用授权码获取 access token 的时候能够验证自身。这也意味着授权服务器可以直接将带有 access token 的响应发送给应用程序后端。一个附加好处是,token 是通过安全地后端通道返回的。虽然授权码最初是为保密客户端优化设计的,但是 PKCE (Proof Key for Code Exchange)使得公共客户端也能使用这种授权方式。

授权码授权 + PKCE

如上图 4 所示展示了 PKCE 的使用。PKCE 可以用于授权和 token 请求以保证请求授权码的应用和使用那个授权码请求 token 的应用是同一个应用。PKCE 防止恶意程序通过截获授权码来获取 access token。

为了使用 PKCE,应用程序需要创建一个机密的随机字符串——code verifier(需要足够的长度以防止被猜中)。应用程序需要进一步从 code verifier 创建一个派生串(derived value)——code challenge(具体方法是将 code verifier 通过 SHA256 哈希处理后进行 BASE64 编码)。当应用程序在第二步获取授权码的时候,需要把 code challenge 及其获取方法伴随 request 一并发送。

当应用程序在第 6 步把授权码发送给授权服务器的 token endpoint 的时候,code verifier 一并发送。授权服务器会使用得到的加密算法对 code verifier 执行转换,并与 code challenge 进行比较,以便确认两次请求是否为同一应用程序。这可以防止恶意程序窃取授权码。

标准提供了 2 种生成 code challenge 的方式:

  • 将 code verifier 通过 SHA256 哈希处理后进行 BASE64 编码
  • plain 方式:即 code verifier 和 code challenge 是相同的,但是这种方法起不到保护效果。

Authorization Request

下面是使用 PKCE 的 API 授权请求示例。它将会被重定向到授权服务器的授权端点(authorization endpoint)。

GET /authorize?
response_type=code
& client_id=<client_id>
& state=<state>
& scope=<scope>
& redirect_uri=<callback uri>
& resource=<API identifier>
& code_challenge=<PKCE code_challenge>
& code_challenge_method=S256 HTTP/1.1
Host: authorizationserver.com

表1 :Authorization Request Parameters

参数 含义
response_type 指示 OAuth 2.0 授权类型。code 表示使用授权码授权。
client_id 应用程序 ID,在向授权服务器注册时分配。
state A non-guessable string, unique for each call, opaque to the authorization server, and used by the client to track state between a corresponding request and response to mitigate the risk of CSRF attacks. It should contain a value that associates the request with the user’s session. This could be done by including a hash of the session cookie or other session identifier concatenated with an additional unique-per-request component. When a response is received, the client should ensure the state parameter in the response matches the state parameter for a request it sent from the same browser.
scope 指示请求授权访问的权限范围。例如:“get:documents”。
redirect_uri 授权服务器会将授权码返回该这个回调 url。
resource 在授权服务器上注册的特定API 的标识符。该参数是在 OAuth 2.0 extension 中的 Resource Indicator 中指定的。一些实现可能使用其它名称“audience”。主要用于自定义 API 的部署。除非存在多个可能的 API,否则不需要此参数。
code_challenge 根据 code_challenge_method 参数指定的方法从 code verifier 计算得到的值。
code_challenge_method 只有 2 种选项: s256plain

CSRF 攻击:

  1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;

  2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;

  3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;

  4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;

  5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

下面详细说一下 OAuth 2.0 中 CSRF 攻击是怎么发生的。主要参考了移花接木:针对OAuth2的CSRF攻击 - 简书 (jianshu.com)

一个针对 OAuth 2.0 进行 CSRF 攻击的例子。假设有用户A,攻击者B,应用程序网站C以及OAuth 2.0 供应商 O。

  1. 攻击者B登录网站C,并选择使用供应商O 来授权
  2. 网站C将攻击者B重定向到供应商O,并询问是否授权
  3. 攻击者同意授权,并截获返回的授权码
  4. 攻击者 B 精心构造了一个 Web 页面,它会触发网站C向供应商 O 发起 access token request,而这个请求中的授权码正是上一步截获的授权码
  5. 攻击者将这个页面放到互联网上,等待或诱导受害者A访问
  6. 受害者在之前已经登录了网站 C,但是没有把自己在网站 C 的账号同 OAuth 2.0 供应商 O 的账号关联起来。受害者A访问攻击者构造的网页,于是 access token request 被顺利触发,但是获得的 token 将会是攻击者 B 的
  7. 所以,网站 C 就会将用户 A 在网站 C 的账号和攻击者 B 在OAuth 供应商 O 的账号关联起来,以后攻击者 B 就可以使用自己在 OAuth 供应商 O 的账号登录受害者 A 在网站C的账户执行操作了

state 参数防御此攻击的原理是:

网站 C 的开发者,只需在OAuth认证过程中加入state参数,并验证它的参数值即可。具体细节如下:

  • 在将用户重定向到OAuth2的Authorization Endpoint去的时候,为用户生成一个随机的字符串,并作为state参数加入到URL中。
  • 在收到OAuth2服务提供者返回的Authorization Code请求的时候,验证接收到的state参数值。如果是正确合法的请求,那么此时接受到的参数值应该和上一步提到的为该用户生成的state参数值完全一致,否则就是异常请求。
  • state参数值需要具备下面几个特性:
    • 不可预测性:足够的随机,使得攻击者难以猜到正确的参数值
    • 关联性:state参数值和当前用户会话(user session)是相互关联的
    • 唯一性:每个用户,甚至每次请求生成的state参数值都是唯一的
    • 时效性:state参数一旦被使用则立即失效

CSRF 攻击成功的前提条件:

尽管这个攻击既巧妙又隐蔽,但是要成功进行这样的CSRF攻击也是需要满足一定前提条件的。

首先,在攻击过程中,受害者A在网站 C 上的用户会话(User Session)必须是有效的,也就是说,A在受到攻击前已经登录了网站 C。

其次,整个攻击必须在短时间内完成,因为OAuth 2.0 供应商颁发的Authorization Code有效期很短,官方推荐的时间是不大于10分钟,而一旦Authorization Code过期那么后续的攻击也就不能进行下去了。

最后,一个Authorization Code只能被使用一次,如果OAuth 2.0 供应商收到重复的Authorization Code,它会拒绝当前的令牌申请请求。不止如此,根据官方推荐,它还可以把和这个已经使用过的Authorization Code相关联的access_token全部撤销掉,进一步降低安全风险。

Response

授权服务器会给返回响应给回调 URL redirect_uri

HTTP/1.1 302 Found
Location: https://clientapplication.com/callback?
code=<authorization code>
& state=<state>

表2 Authorization Response Parameters

参数 含义
code 返回的授权码
state 在 authorization request 中发送过来的 state (无任何修改)。应用程序需要验证与发送的 state 值相同。

请求 token endpoint

在得到授权码之后,需要向 token endpoint 发起请求了。

POST /token HTTP/1.1
Host: authorizationserver.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
& code=<authorization_code>
& client_id=<client id>
& code_verifier=<code verifier>
& redirect_uri=<callback URI>

表 3 Token Request Parameters

参数 含义
grant_type 对于授权码授权来说,此值为 authorization_code
code 之前接收到的授权码
client_id 在授服务器注册时得到的 ID
code_verifier 长度在 43-128 之间的随机字符串(包含 A-Z, a-z, 0-9, “-“, “_”, “.” 和 “~”)。授权服务器会用之前收到的 code_challenge_method 对其进行处理并于之前收到的 code_challenge 进行比较。
redirect_uri 授权服务器响应的回调 URI。应该和 authorize endpoint request 的 redirect_uri 匹配。

从 token endpoint 返回的响应类似下面的结构:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
     {
       "access_token":"<access_token_for_API>",
       "token_type":"Bearer",
       "expires_in":<token expiration>,
       "refresh_token":"<refresh_token>"
     }

表 4: Token Endpoint Response Parameters

参数 含义
access_token 用于访问 API 的 access token。不同的授权服务器返回的 access token 会有所不同。
token_type token 类型,比如“Bearer”
expires_in token 的有效时长
refresh_token Refresh token 是可选的。refresh token 用于更新 access token。本文后面会进行详细介绍。

4.2 隐藏式授权 (Implicit)

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)。它是在 CORS(跨域资源共享,Cross-Origin Resource Sharing)广泛应用之前设计的,这意味着应用程序只能调用同一 domain 的资源,而无法访问授权服务器的 token endpoint。To compensate for this limitation, the implicit grant type has the authorization server respond to an authorization request by returning tokens to the application in a redirect with a URL hash fragment. 隐式授权类型的交互如图5所示。

图5:OAuth 2.0 Implicit Grant

  1. 用户(资源所有者)访问应用程序
  2. 应用程序将用户浏览器重定向到授权服务器的 authorize endpoint 并发送授权请求
  3. 授权服务器提示用户及进行身份验证并同意
  4. 用户进行身份验证并同意授权请求
  5. 授权服务器重定向回应用程序的回调 URL 并返回 access token
  6. 应用程序使用 access token 来进行 API 访问

现在大多数浏览器都支持 CORS,因此隐藏式授权最初的需求就不存在了。此外,在 URL hash fragment 中返回 access token 有潜在的泄露风险(浏览器记录或者 referer header),因此这种方式不在推荐使用了。

值得注意的是,最初的 OAuth 2.0 规范发布之后,the OAuth 2.0 Multiple Response Type Encoding Practices specification 定义了一个 response_mode 参数,这允许授权服务器以新的方式返回响应。后续的规范定义了新的响应机制。比如,OAuth 2.0 Form Post Response Mode 将响应编码为 HTML form,通过 HTTP-POST 发送回应用程序。

Authorization Request

GET /authorize?
response_type=token
& response_mode=form_post
& client_id=<client_id>
& scope=<scope>
& redirect_uri=<callback uri>
& resource=<API identifier>
& state=<state> HTTP/1.1
Host: authorizationserver.com

各参数的含义跟授权码授权是一样的,response_type=token 指明该请求使用隐藏式授权方式,response_mode 设置为 from_post

一个使用默认响应模式的隐藏式授权请求授权成功后,返回响应到 redirect_uri ,并在 response 的 URL fragment 中包含 access token, token type, token expiration 以及 state。URL 片段可以通过 header 和 浏览器历史记录得到,使用 response_mode=form_post 可以将 response 表单编码后返回。

4.3 密码式授权(Password Credentials Grant)

密码式授权主要适用于极度信任应用程序并且没有其它可行的授权方式时。但是不建议使用这种授权类型,因为直接将凭证暴露给应用程序有泄露的风险。它主要被用于遗留的嵌入式登陆页面以及用户迁移的场景。这种方式不涉及用户同意的步骤,用户没有办法限制应用程序的访问范围,凭证很可能被应用程序滥用。

用于用户迁移的场景:

如果用户需要使用不兼容的哈希算法从一个 identity repository 迁移到另一个 identity repository,则新系统可以提示用户提供凭证,使用旧系统的 identity repository 及散列算法验证通过后,再使用新的散列算法将其存储到新系统,并从旧系统检索其它相关的用户信息一并存储到新系统。使用这种授权方式,client 需要再获得 access token 之后立即丢弃用户凭证,以减少泄露风险。

图6:Password Credentials Grant

  1. 用户(资源所有者)访问应用程序
  2. 应用程序提示用户输入用户凭证
  3. 用户直接把凭证(密码等)提供给应用程序
  4. 应用程序使用用户凭证向授权服务器发送 token request
  5. 授权服务器返回 token(不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应)
  6. 应用程序使用得到的 token 访问 API

Authorization Request

POST /token HTTP/1.1
Host: authorizationserver.com
Authorization: Basic <encoded application credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=password
& client_id=<client_id>
& scope=<scope>
& resource=<API identifier>
& username=<username>
& password=<password>

参数含义与授权码授权的参数含义相同,区别在于grant_type=password,以及从用户得到的 usernamepassword。请求成功后,授权服务器返回 access token。

4.4 凭证式授权(Client Credentials Grant)

这种授权方式适用于代表应用程序本身访问 API,因此不涉及与应用程序的用户的交互。应用程序使用其自身的凭证从授权服务器获取 token。

图7:Client Credentials Grant

  1. 应用程序向授权服务器发送包含应用程序凭证的授权请求
  2. 授权服务器验证请求中的 credential 并返回 access token
  3. 应用程序使用得到的 access token 访问 API
  4. 如果 access token 过期,那么重复上述步骤。

The Authorization Request

POST /token HTTP/1.1
Host: authorizationserver.com
Authorization: Basic <encoded application credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
& scope=<scope>
& resource=<API identifier>
& client_id=CLIENT_ID
& client_secret=CLIENT_SECRET

5. 调用 API

应用程序得到 access token 之后,调用 API 时需要传入这个 token。传入的方式因 API 而异,但是典型的方式时通过名为 Authorization 的 HTTP request header,并使用 Bearer authorization token type:

GET /api-endpoint HTTP/1.1
Host: api-server.com
Authorization: Bearer <access_token>

access token 有过期时间,但并不是一次性的,所以为了性能优化,应用程序可以缓存 access token,并一直使用它直到过期,以避免每次访问 API 都要授权服务器授权。

6. Refresh Token

OAuth 2.0 的 access token 有一个过期时间,当 access token 过期时,应用程序可以发起一个新的 authorization request,但是 OAuth 2.0 提供了另一种方式。refresh token用于在 access token 过期时,从授权服务器得到新的 access token。

并非所有的场景都需要 refresh token。比如对于凭证式授权就不需要,因为应用程序可以在任何时间代表其自身获得 token,不涉及和用户的交互。此外,static refresh token 也不适合用在 public client,因为 refresh token作为敏感数据不适合存储在 public client。OAuth 2.0 Threat Model and Security Considerations document 提出了 refresh token rotation 的概念,以检测 refresh token 是否被窃取,是否被多个 client 使用。此方案要求授权服务器为每次 access token 更新请求都返回一个新的 refresh token。OAuth 2.0 Security Best Current Practice document 要求对于 public client,授权服务器必须使用 refresh token rotation 或者 sender-constrained refresh token(bound to a particular client)来降低 refresh token 被破坏的风险。

refresh token 为 web 应用和原生应用提供了更新 access token 的便捷方法,在 access token 过期后自动更新可能很诱人,但是根据最少特权原则,最好只有在必要的时候更新 access token。

OAuth 2.0 规范并没有规定应用程序请求 refresh token 的规则,授权服务器自行决定是否发布 refresh token,有些授权服务器可能会自动发出 refresh token,有些则可能要求显式请求时才发布 refresh token。撤销 access token 在 OAuth 2.0 中不是必须的,所以有些授权服务器可能不支持撤销 access token。下面的例子展示了使用 refresh token 向授权服务器的 token endpoint 请求新的 access token 的请求(client_idclient_secret 用于应用程序验证自身):

POST /token HTTP/1.1
Host: authorizationserver.com
Authorization: Basic <encoded application credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
& client_id=<CLIENT_ID>
& client_secret=<CLIENT_SECRET>
& refresh_token=<refresh_token>

access token 将在响应中返回(与前几节中描述的类似)。scope参数是可选的,如果使用,必须等于或小于原始授权请求中的范围,并且传递的客户端凭据(client_idclient_secret )必须是发出原始授权请求的应用程序的凭据。

7. 总结

本文介绍了应用程序如何通过 OAuth 2.0 为 API 授权。供应商的 SDK 可能简化某些交互过程,或者使用不同的参数名,具体信息需要查阅您选用的授权服务器文档。但无论如何,了解原理都是很有必要的。另外,查阅 OAuth 2.0 规范也是很有意义的,因为规范中列出了所有支持的参数以及高级用法。

access token 会被 API 使用,access token 的形式可能会不同,但是应用程序不应该依赖 access token 中的数据。接受 access token 的 API 在执行其请求之前必须先验证 access token 的有效性,验证过程因授权服务器的实现会有所不同。


文章作者: Shichao Zhang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Shichao Zhang !
  目录