04. 兵马未动粮草先行:向授权服务器申请一个身份凭证

我在上一小节向你描述了OAuth 2 定义的四种角色。其中授权服务器必须持有和管理客户端的信息,当客户端执行授权请求或申请访问令牌时授权服务器可以用某种方式认证当前的客户端,这样授权服务器才能识别当前是哪个客户端来代表用户申请授权。你可以认为这是 OAuth 安全的基石,授权服务器负责管理和维护客户端,并不是所有的应用都可以代表用户获取终端用户托管在资源服务器上的资源,授权服务器只会与经过预认证和注册的客户端发生关系。当你(往往是开发者)想要与一个提供 OAuth 2 服务的供应商发生关系时,那么第一步,你必须向OAuth 2 服务供应商申请和注册一个客户端。
当注册客户端时,最基本的客户端开发者应该:

  1. 指定客户端类型。
  2. 提供客户端重定向URI。
  3. 提供授权服务器要求的其他信息(例如,客户端名称、客户端Logo、客户端简介等信息)。
    作为授权服务提供商,在客户端注册流程结束时,你应该提供如下信息:
  4. 客户端ID:客户端应用程序的唯一标识符。
  5. 客户端密钥:客户端密钥仅仅是被授权服务器和客户端应用程序所知道的信息。客户端应用程序应该安全地保存和使用客户端密钥。一旦客户端秘钥发生泄露,客户端应用程序应该立即向授权服务器申请重置或撤销该客户端密钥。授权服务器相应的也应该提供对应这种能力,并且向该客户端颁发的所有访问令牌或刷新令牌都将被失效。
  6. 开发者提供的重定向URI。
  7. 授权服务器元信息:OAuth 2.0客户端与授权服务器交互的一切必要信息,包括授权服务器的端点位置(比如授权端点、令牌端点)以及授权服务器的能力信息等。RFC 8414 文档(https://datatracker.ietf.org/doc/html/rfc8414)描述了授权服务器应该向接入客户端暴露的元信息内容和提供方式。这也是我们讨论的授权服务器六大端点中的一个端点——授权服务器元信息端点 (OAuth2 Authorization Server Metadata Endpoint),我将在授权服务器实现的章节与你讨论这些细节。

在微博开放平台注册一个客户端

如下图所示,在新浪微博开放平台(https://open.weibo.com/developers)完成开发者身份认证后,你就可以创建应用。
image

注册应用的第一步,你需要输入应用名称和选择应用分类,即客户端类型,在后文我将与你讨论OAuth 2.0规范的客户端类型及要求开发者选择客户端类型的必要性。
image

在下一步你需要填写关于应用的更多信息,这些信息通常取决于具体的授权服务提供商。但是一般的会要求填写客户端名称、简介、客户端Logo等信息。注意在这一步里新浪微博开放平台向我们生成和下发了最重要的AppKey和App Secret,即在OAuth 2.0里定义的client_id和client_secret 。在后文我们将使用这里得到的凭证向授权服务器表明身份获取授权。
image

同时,最重要的一步,需要开发者填写回调地址(redirect_uri),我将在下文为你详细解释这个参数的作用和意义。在后文中你也会看到它的重要性。
image

客户端类型

在上面我们看到创建应用的第一步,就是选择客户端类型。OAuth 2.0基于客户端是否具有安全存储客户端密钥的能力定义了两种类型的客户端,即机密客户端(confidential client)和公共客户端(public client).

  1. 机密客户端(confidential client):是指能够安全的存储自己的客户端ID、客户端秘钥以及访问令牌等机密信息的客户端应用程序。机密客户端通常是由客户端-服务器-数据库组成的服务端应用程序,后端服务器通常可以保证机密信息的安全存储和传输。
  2. 公共客户端(public client):不能安全的存储客户端密钥等信息的客户端应用程序。例如直接在浏览器中运行的纯JavaScript应用程序或移动APP,它们直接从浏览器向授权服务器发起所有的请求,保存在浏览器中的任何信息对终端用户都是可见的。
    值得注意的是在OAuth 2.1中定义了第三种客户端类型,即认证客户端(credentialed client),这种客户端拥有客户端ID和客户端密钥,但是没有经过授权服务器认证。

一般来说,注册客户端时授权服务器需要客户端背后的责任人(比如应用的开发者)提供一定的资质信息并且经过授权服务器相关方审核资质信息的真实性和合法性后才会正式颁发客户端ID和客户端秘钥。例如下图如果你想入驻抖音开放平台,你必须经过以下过程:
image

这些资质信息由OAuth服务供应商规约,不同的服务供应商有不同的实现。
在OAuth 2.0提供了一种向授权服务器动态注册客户端的能力,RFC 7591文档(https://tools.ietf.org/html/rfc7591)描述这一能力。该协议说明了对于支持RFC 7591协议的授权服务器需提供一个动态注册客户端应用程序的端点,该端点可以选择性地关闭或打开。如果它是开放的,任何有权访问环境的人都可以通过该端点动态注册应用程序。在OAuth 2.1中细分了客户端类型,将通过这种方式注册的客户端为credentialed client,即认证客户端。它同机密客户端一样,具有client_id和client_secret,但是在生成和签发这些凭证信息的时候,并没有经过授权服务供应商的审核和校验。

向开发者问询客户端类型的必要性

注意我们在新浪微博开放平台创建客户端的第一步,新浪微博客户端向我们问询我们要创建的客户端分类,即客户端类型。如下图所示:
image

这样有什么必要性呢?
原则上如果用户创建的是一个公共客户端,例如一个本地移动应用程序,那么授权服务供应商应该避免向这种客户端发放客户端密钥。主动询问和提示开发者,有助于帮助开发者正确地使用客户端密钥或授权类型,降低泄密的可能性。主动询问的另一个原因在于,对于不同的客户端类型,它们的重定向地址(redirect_uri)的格式是不同的,例如对于本地移动应用程序必须打开系统浏览器并提供本地重定向URI 来处理来自授权服务器的响应。我将在扩展篇与你讨论这些技术的细节,在这之前我们讨论的都是基于浏览器的web应用,它们在技术上没有什么本质的区别,所以不必担心。

OAuth 2支持的客户端类型

一般的,OAuth 2是围绕以下三种客户端类型进行设计和展开的。

  1. Web应用程序(web application)
    web应用程序是web服务器上运行的机密客户端。它能够安全地存储自己的数据,因此它能够安全地存储客户端密钥信息和访问令牌,这些信息对资源拥有者是不可见或不可访问的。
  2. 基于用户代理的应用程序(user-agent-based application)
    基于用户代理的应用程序是公共客户端,这种客户端应用程序从web服务器下载客户端代码并在资源所有者使用的设备上的用户代理(例如web浏览器)中运行。例如在一个在浏览器上运行的单页面应用程序,它直接从浏览器向授权服务器发起所有请求,终端用户可以看到在浏览器中保存的任何信息,因此,单页面应用程序是公共客户端的一个示例。OAuth 2规范推荐使用PKCE技术来增强在基于用户代理的应用程序中使用OAuth 2的安全性,我将在第二关的第(21)小节演示如何使用PKCE技术来增强在单页面应用程序中使用OAuth 2的安全性。
  3. 本地应用(Native application)
    本地应用程序也被视为是公共客户端的。对设备具有root访问权限的用户可以发现隐藏在本地应用程序中的任何机密信息。我们将在第三关中讨论这种应用程序中如何使用OAuth 2。

重定向URI

我在在微博开放平台注册一个客户端小节的最后一个部分,向你说明了向授权服务器声明和注册重定向URI是最重要的一步,笔者甚至认为它是OAuth安全的基石。OAuth授权服务器与客户端交互时仅会通过客户端已向授权服务器声明和注册的重定向URI,授权服务器通过这个重定向URI将一些重要的信息,例如授权码流程中非常重要的中间介质授权码就是通过重定向URI完成交互和传递的 。授权服务器仅将用户重定向到对应客户端已注册的URI,以防止恶意程序和开发者针对授权码或访问令牌的重定向攻击。

一般的,授权服务器可能会允许一个客户端一次性注册多个重定向URI,这种机制的一个可能原因在于你的客户端应用程序在开发、测试以及生产环境的授权回调地址一般是不同的,允许一个客户端一次性注册多个授权回调地址可以解决这种多环境问题。如果授权服务供应商允许一次性注册多个重定向URI,那么在授权请求和令牌请求中,客户端应用程序必须显示指定使用的URI。

对于重定向URI另外一个重要的原则是授权服务提供商必须强制要求客户端开发者提供和注册的重定向URI是一个https端点,以保证授权服务器和客户端在交互过程中的传输安全。如果授权服务提供商允许使用非https的端点,那么他们必须采取额外的预防措施,以确保这种攻击是不可能的。

向我们自定义实现的授权服务器注册一个新的客户端

如下图所示,如果你想要在我们自定义实现的授权服务器注册一个新的客户端,你需要填写以下信息。

  1. 客户端名称。
  2. 客户端类型。在上面我们讨论了声明客户端类型的必要性。
  3. 重定向URI(支持多个重定向URI)。定义客户端的重定向URI端点。
  4. 允许的权限范围。定义和限制客户端能够使用权限范围。
  5. 允许的授权模式。定义和限制客户端能够使用的授权模式。
    我们还支持定义其他一些高级的参数,包括:
  6. 授权服务器令牌端点支持的客户端认证方式。当授权服务器的令牌端点对客户端进行认证时将会使用该配置。
  7. 指定授权服务器的授权响应模式。定义授权服务器的授权端点响应数据的形式。
  8. 访问令牌生命周期。当授权服务器的令牌端点签发访问令牌时将会使用该配置。
  9. 是否支持刷新令牌以及刷新令牌生命周期。当授权服务器的令牌端点签发刷新令牌时将会使用该配置。
  10. 应用简介。这些内容有可能会展示在用户确认授权的界面。
    image

动态客户端注册

在本小节的最后我们讨论一种动态客户端注册的技术,通过这种方式注册的客户端我们称为认证客户端(credentialed client)。提供这种技术的目的是为了支持在某些场景下的客户端实时注册。例如安装在 Android 或者 IOS 中的本地应用程序,它们在与 OAuth 工作时使用动态客户端注册技术动态地申请客户端凭证信息(包括客户端ID和客户端密钥)是一种较好地安全实践。我们将在第三关的第24小节对其进行详细的讨论。下面我们补充性地对这种技术进行一些简单的讨论。你可以在 RFC 7591 文档中获取对这种技术的更充分的描述(https://www.rfc-editor.org/rfc/rfc7591)。

要动态注册一个客户端,客户端或开发人员向授权服务器的客户端注册端点发起 HTTP POST 的请求将客户端元数据传递给授权服务器,下面是一个动态客户端注册请求的示例。

`POST /register HTTP/1.1
Content-Type: application/json
Accept: application/json
Host: server.example.com

 {
  "redirect_uris": [
    "https://client.example.org/callback",
    "https://client.example.org/callback2"],
  "client_name": "My Example Client",
  "client_name#ja-Jpan-JP":
     "\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u540D",
  "token_endpoint_auth_method": "client_secret_basic",
  "logo_uri": "https://client.example.org/logo.png",
  "jwks_uri": "https://client.example.org/my_public_keys.jwks",
  "example_extension_parameter": "example_value"
 }

我们讨论一下这些参数的含义。

  1. redirect_uris:重定向URI数组。在获得授权许可后,用户将被重定向到这些 redirect_uris 之一。这些重定向URI必须是HTTPS端点。
  2. token_endpoint_auth_method:与授权服务器的令牌端点交互时的客户端身份验证方案。一般有五种值(我们在授权服务器实现的章节还会对这些客户端认证方式进行更详细地讨论和定制):
  • none: 这是 OAuth 2.0 定义的公共客户端,它没有客户端密钥。
  • client_secret_post:客户端使用 HTTP POST 方法在请求体中传递客户端ID和客户端密钥。
  • client_secret_basic: 客户端使用 HTTP Basic 方法传递客户端身份信息。
  • client_secret_jwt 和 private_key_jwt:通过 JWT 声明的方式进行客户端认证。
  1. grant_types:客户端支持的授权类型数组。
  2. response_types:来自授权服务器的预期响应类型的数组。grant_type 和 response_type 之间存在关联性,我在介绍四种授权类型时会介绍这些细节。
  3. client_name:客户端名称。
  4. client_uri:提供有关客户端信息的网页的URL。如果存在,服务器应以可点击的方式向最终用户显示此 URL。
  5. logo_uri:指向客户端应用程序 logo 的URL。在登录流程中,授权服务器将向终端用户展示该 logo。
  6. scope:以空格分隔的作用域值列表,客户端准备从授权服务器请求这些值。我将在下一小节讨论该参数的含义。
  7. contacts:表示该客户端的负责人的联系方式,通常是电子邮件地址。
  8. tos_uri:指向客户端应用程序的服务条款文档的 URL。在登录流程中,授权服务器将向最终用户显示此链接。
  9. policy_uri:指向客户端应用程序的隐私策略文档的 URL。在登录流程中,授权服务器将向最终用户显示此链接。
  10. jwks_uri:指向客户端的 JWTSet 的端点。授权服务器使用此公钥来验证由客户端应用程序签名的任何请求的签名
  11. software_id:和 client_id 类似,但它是由客户端开发人员或软件发布者分配的,用于在客户端动态注册的过程中向授权服务器的客户端注册端点标识客户端软件。而 client_id 由授权服务器生成,主要用于标识应用程序。但是client_id可以在应用程序的生命周期内更改。相比之下,software_id 在应用程序的整个生命周期中是唯一的,并且在整个应用程序生命周期中唯一地表示与之相关的所有元数据。
  12. software_version:客户端应用程序的版本。
  13. software_statement:这是注册请求中的一个特殊参数,它携带一个 JWT 令牌。此 JWT 包括先前针对客户端定义的所有元数据。如果在 JWT 中以及在 software_statement 参数之外的请求中定义了相同的参数,则以software_statements 中的参数为准。

授权服务器可以确定是否接受该注册请求。即使授权服务器接受请求,授权服务器也不会认同注册请求中所携带的所有建议参数。grant_types 是一个例子,例如,客户端可能想注册和使用 authorization_code 和 implicit 授权类型,授权服务器对此有最终的决定权。它有可能只接收 authorization_code 类型,因此它忽略了客户端注册 implicit 类型的请求。token_endpoint_auth_method 是另一个例子,授权服务器有可能只支持一种客户端认证的方法,这具体取决于授权服务器的实现。

`HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
"client_id": "s6BhdRkqt3",
"client_secret": "cf136dc3c1fc93f31185e5885805d",
"client_id_issued_at": 2893256800,
"client_secret_expires_at": 2893276800,
"redirect_uris": [
"https://client.example.org/callback",
"https://client.example.org/callback2"],
"grant_types": ["authorization_code", "refresh_token"],
"client_name": "My Example Client",
"client_name#ja-Jpan-JP":
"\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u540D",
"token_endpoint_auth_method": "client_secret_basic",
"logo_uri": "https://client.example.org/logo.png",
"jwks_uri": "https://client.example.org/my_public_keys.jwks",
"example_extension_parameter": "example_value"
}
`
参数的含义如下:

  • client_id:为客户端生成的唯一标识符。
  • client_secret:生成的与client_id对应的客户端密钥。这是可选的。例如,对于公共客户端,不需要client_secret。
  • client_id_issued_at:从1970年1月1日起的秒数。
  • client_secret_expires_at:从1970年1月1日起的秒数,如果未过期,则为0。
  • redirect_uris:接受的重定向数组。
  • grant_types:接受的授权类型数组。
  • token_endpoint_auth_method:令牌端点接受的客户端身份验证方法。
    总结
    [] 如果客户端想要和授权服务器发生关系,那么第一步也是最重要的一步——你需要向授权服务器申请和注册一个新的客户端。在注册完成后,你将会得到客户端凭证信息——客户端ID(client_id)和可选的客户端密钥(client_secret)。
    [] 作为授权服务提供商,在客户端注册的过程中需要主动询问客户端开发者即将要创建的客户端类型。对于公共客户端,在客户端注册完成后不应该颁发客户端密钥(client_secret)。这会降低客户端开发者错误地使用客户端密钥的可能性,从而减少客户端密钥泄露的风险。
    [] 动态客户端注册是OAuth生态系统中非常优秀的扩展。它提供了一种便捷获得客户端凭证的方法。
    下一小节我将会向你介绍另外两个基础但重要的概念——作用域(scope)和访问令牌(access token)。毋庸置疑访问令牌的重要性,因为在前面的小节中你已经了解到 OAuth 的核心就是使用一个有时限的令牌来替代用户凭证信息。而作用域跟访问令牌息息相关,一个访问令牌总是绑定若干个作用域。
posted @ 2023-05-07 11:21  小米粥|  阅读(81)  评论(0)    收藏  举报