10. 不再被推荐的模式:简化模式和密码模式

在第(6)小节和第(9)小节我分别向你介绍了OAuth 2.0定义的两种重要的授权模式——授权码模式和客户端模式。在这一小节我向你介绍OAuth 2定义的另外两种授权模式——简化模式和密码模式,正如我在第(1)小节描述的,这两种授权模式在OAuth 2.1中已经被完全废弃,因为这两种模式已经被验证为不安全和充满不确定性的授权模式。但是考虑到历史原因,你也许还在使用这两种授权模式,因此我还是向你介绍这两种授权模式,并且后面我们实现的的授权服务器,也会支持这两种授权模式。但是我建议你遵循OAuth 2.1规范的最佳实践,改用更安全的授权码模式。

简化模式

和授权码模式相比,简化模式被设计和使用在客户端应用程序(Client-side Applications),这种应用程序也是我在第(4)小节中向你介绍的公共客户端,它们主要使用HTML和JavaScript编写,并且在执行之前所有的代码必须被下载到客户端(一般是浏览器)中运行,因此它们无法安全地保存客户端密钥、访问令牌、刷新令牌等这些机密信息。在这种模式下,在客户端注册过程中向授权服务器预注册和声明的重定向URI(redirect_uri)是保障这种模式安全性的最主要的手段。很明显的,因为它们无法安全地存储刷新令牌,因此无论如何,授权服务器在这种模式下都不会向客户端生成和响应对应的刷新令牌。
 
我在第(4)小节介绍授权码模式的时候,顺便说明了简化模式。理解了授权码模式也就理解了简化模式。和授权码模式相比,简化模式究竟简化了什么内容呢?它简化了使用授权码(authorizaiton code)交换访问令牌的过程,因此它只需要向授权服务器发起一次请求。下面是简化模式的时序图。
 
 
  1. 终端用户通过浏览器访问第三方应用。
  2. 第三方应用程序初始化授权请求将用户浏览器重定向到授权服务器的授权端点。
  3. 如果用户没有在授权服务器的域下登录,则授权服务器将引导用户进行登录。否则直接向用户展示授权页面。
  4. 用户登录并且同意授权。
  5. 授权服务器将用户浏览器重定向回第(1)步授权请求中的授权回调URI,并通过Fragment的方式直接返回访问令牌。
  6. 第三方应用程序使用上面获取到的访问令牌向资源服务器的资源端点发起资源调用请求,换取终端用户的资源。
  7. 资源服务器向第三方应用程序返回资源调用结果。
  8. 第三方应用程序处理用户资源,并将结果通过浏览器返回给终端用户。

授权请求

对于简化模式而言,客户端只需要初始化一次请求,我们称这样的请求为授权请求(Authorization Request)。下面是一个授权请求的示例:
 GET /authorize?
 response_type=token&
 client_id=s6BhdRkqt3&
 state=xyz&
 scope=read write&
 redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1

Host: server.example.com

授权请求的参数含义如下:
  • response_type:必须参数。在简化模式下response_type的值必须为token,表明客户端在这种模式下期望授权服务器向它直接响应一个访问令牌(Access Token)。
  • client_id:必须参数。客户端的唯一标识,这是客户端注册的主要产物。
  • state:推荐参数。客户端负责该参数的初始化,授权服务器会原封不动地将该参数的值返回给客户端。该参数可以有效防范可能发生的跨站请求伪造攻击(CSRF)。我们在OAuth 2.0的安全篇还会向你详细介绍该参数的详细使用说明。
  • redirect_uri:该参数表示用户在授权完成后授权服务器向客户端返回响应的端点,无论是返回访问令牌(Access Token)的成功响应,还是返回错误消息的异常响应。该参数值必须与OAuth 2.0客户端注册时提供的授权回调URI完全匹配。如果该值与提供的client_id对应的授权重定向URI不匹配,则会得到一个redirect_uri_mismatch的错误。
  • scope:可选参数,表示客户端请求授权的访问权限范围,我们在第(5)小节详细讨论了该参数。一般是以空格分隔、区分大小写的字符串列表,例如“read write”。该参数具体需要参阅授权服务方提供的用户文档。

成功响应

当授权服务器的授权端点收到上面客户端初始化的授权请求后,最主要的它将验证client_id和redirect_uri参数的合法性,如果这些参数有效,它将向客户端返回成功的响应。下面是一个成功响应的示例:
 HTTP/1.1 302 Found
 Location: http://example.com/cb#
 access_token=2YotnFZFEjr1zCsicMWpAA&
 state=xyz&
 token_type=example&
 expires_in=3600&
 scope=read write
值得注意的是,在简化模式下响应的所有重要信息都包含URI片段中(URI Fragment),RFC 3986规范定义了Fragment的详细语法。这是OAuth工作组尽量减少简化模式可能存在的安全风险的最大努力,如果浏览器对一个带有Fragment的地址发起Ajax请求,那么Fragment的部分不会被发送到服务器,因此只有颁发令牌的授权服务器和Web浏览器能获取到访问令牌的值,但是Fragment的部分仍然可以通过referer头和浏览器历史记录获取。
  • access_token:必须参数。这是最重要的参数,客户端最终代表终端用户访问终端用户资源的“钥匙”。
  • state:如果客户端初始化的授权请求中有该参数,那么授权服务器应该原封不动地将该参数值返回给客户端。
  • token_type:必须参数。授权服务器颁发的令牌的类型。一般该值是“bearer”。如果你对Bearer Token感兴趣,你可以阅读RFC 6750文档(https://datatracker.ietf.org/doc/html/rfc6750)。
  • expires_in:可选参数。以秒为单位的访问令牌的生存期。该参数为非必需参数,如果缺省这个参数,授权服务器必需以其他方式告知客户端令牌的剩余生命周期。
  • scope:可选参数。表示客户端请求授权的访问权限范围。如果授权服务器颁发的访问令牌绑定的权限范围和授权请求中客户端申请的权限范围一致,则可以缺省该参数。否则授权服务器必须显示告知权限范围发生变化。

错误响应

如果用户拒绝授权或在授权请求的过程中发生其它异常,比如客户端不存在或者授权回调地址不匹配等,客户端的授权回调地址将会收到如下的错误响应(这是OAuth 2.0文档规范的内容,各个授权服务器供应厂商应该遵循该规范。我们实现的授权服务器同样遵循这样的规范)。下面是一个错误响应的示例:
HTTP/1.1 302 Found
Location: https://client.example.com/cb#
error=access_denied&
state=xyz
  1. error参数是必须的,用来辅助开发者来定位问题,error通常是以下错误码之一:
    1. invalid_request:非法的授权请求。授权请求中可能缺失必要的参数或者提供的参数值异常,或者重复提供一个参数或不可辨识的授权请求。
    2. unauthorized_client:授权服务器无法认证该客户端。
    3. access_denied:终端用户拒绝了来自客户端的授权请求。
    4. unsupported_response_type:非法的授权类型(response_type)。在简化模式下该值必须设置为token,否则授权服务器认为该授权类型非法。
    5. invalid_scope:请求的作用域无效、未知或格式不正确。
    6. server_error:授权服务器内部错误。
    7. temporarily_unavailable:授权服务器暂时不可用。
  2. error_description:可选参数。人类可读的描述错误原因的信息,用于帮助客户端开发人员了解发生的错误。
  3. error_uri:可选参数。指向包含有关错误详细信息的web文档的链接。
  4. state:该参数是必须的。如果授权请求中存在state参数,那么即使授权请求失败,授权服务器的授权端点也应该在响应中返回该参数。

使用授权码模式和PKCE来代替简化模式

在上面我向你介绍了在OAuth 2规范定义的一种在公共客户端中使用的授权模式——简化模式,然而正如我在前文向你介绍的,这种模式已经在OAuth 2.1中被彻底废弃,更应该使用授权码模式和PKCE来代替简化模式。下面我向你介绍如何在公共客户端中使用授权码模式和PKCE技术来代替简化模式。如下面的时序图所示,和授权码模式一样,它同样需要初始化和发送两个请求——授权请求和令牌请求,但是这里的第三方应用是一个单页面应用程序,它没有安全存储和传输机密数据的能力。
  1. 终端用户通过浏览器访问第三方应用,这个应用程序是一个单页面应用程序,它没有安全存储和传输机密数据的能力。
  2. 第三方应用程序初始化授权请求将用户浏览器重定向到授权服务器的授权端点。这里的授权码请求和我在第(8)节中描述的授权码请求并没有差异,但是在单页面应用程序中需要将生成的code_verifier保存在LocalStorage或者SessionStorage中。下面是一个授权请求的示例:
GET /oauth/authorize? 
 response_type=code
 & client_id=[CLIENT_ID]
 & state=[STATE]
 & scope=[SCOPE]
 & redirect_uri=[REDIRECT_URI]
 & code_challenge=[CODE_CHALLENGE]
 & code_challenge_method=S256 HTTP/1.1 
 Host: authorizationsever.com
  1. 如果用户没有在授权服务器的域下登录,则授权服务器将引导用户进行登录。否则直接向用户展示授权页面。
  2. 用户登录并且同意授权。
  3. 授权服务器将用户浏览器重定向回第(2)步授权请求中的授权回调URI,并且附带了一个临时的授权码。
  4. 第三方应应用程序初始化令牌请求,向授权服务器的令牌端点发送令牌请求,目的是使用上一步获得的授权码换取代表终端用户访问资源的“钥匙”——访问令牌。这和我在第(8)节中描述的令牌请求的内容并没有差异,但是单页面应用程序并没有客户端密钥(client_secret)。下面是一个令牌请求的示例:
POST /token HTTP/1.1
Host: authorizationsever.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=[REDIRECT_URI]

  1. 授权服务器向第三方应用程序颁发访问令牌(因为单页面应用程序无法安全地保存刷新令牌,因此在这种模式下授权服务器不应该向应用程序颁发刷新令牌)。
  2. 第三方应用程序使用上面收到的访问令牌向资源服务器的资源端点发起资源调用请求,换取终端用户的资源。
  3. 资源服务器向第三方应用程序返回资源调用结果。
  4. 第三方应用程序处理用户资源,并将结果通过浏览器返回给终端用户。

密码模式

密码模式是我们之前讨论的被OAuth 2.1废弃的另外一种授权模式。OAuth 2.1摒弃这种授权模式的原因显而易见,在这种模式下需要向第三方客户端提供终端用户的身份凭证信息——用户名(username)和密码(password )。实际上这正是我们在第(1)小节讨论的OAuth诞生之前,解决共享终端用户资源的唯一方式。这种授权模式的存在可能让使用者感到困惑,共享密钥不正是OAuth竭力避免和要解决的问题么?但是实际上,这种授权模式被设计的唯一原因是让原来使用 HTTP Basic身份认证的应用程序平滑迁移到OAuth 2。HTTP Basic身份认证是这样一种模式,它需要每个API请求中都携带用户的身份凭证信息,而使用OAuth 2.0提供的密码模式,终端用户只需要提供一次身份凭证信息,在获得访问令牌后就不需要终端用户重复提供身份凭证信息。使用这种模式我们需要假设客户端应用程序是高度可信的,客户端应用程序收到终端用户的身份凭证信息的唯一目的是交换一个临时的访问令牌而不“偷偷”保存用户的凭证信息。
 
下面是OAuth 2.0密码模式工作的时序图。和我们之前描述的客户端模式一样,这种模式下不需要重定向URI(redirect_uri)参数的参与,而只需要第三方应用程序后端向授权服务器的令牌端点发起一次HTTP调用。
  1. 终端用户通过浏览器访问第三方应用。
  2. 第三方应用要求用户提供他在授权服务器侧的身份凭证信息(username&password)。
  3. 用户提供他在授权服务器侧的身份凭证信息。
  4. 第三方应用初始化令牌请求,向授权服务器的令牌端点发送请求(注意这是一次后端HTTP调用)。
  5. 授权服务器向第三方应用程序后端返回具有有限生命周期的访问令牌。
  6. 第三方应用程序使用上面的访问令牌向资源服务器的资源端点发起资源调用请求。
  7. 返回终端用户资源。
  8. 第三方应用程序处理用户资源,并将结果通过浏览器返回给终端用户。

令牌请求

对于密码模式,客户端应用程序只需要向授权服务器的令牌端点发起一次请求,我们称这样的请求是令牌请求(Access Token Request)。下面是一个OAuth 2.0密码模式的令牌请求示例:
 POST /token HTTP/1.1
 Host: server.example.com
 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
 Content-Type: application/x-www-form-urlencoded

grant_type=password&username=johndoe&password=A3ddj3w

  1. grant_type:必须参数。 在密码模式下该值必须设置为password
  2. username:必须参数。资源拥有者的用户名。
  3. password: 必须参数。资源拥有者的密码。
  4. scope:可选参数,表示客户端请求授权的访问权限范围,我们在第(5)小节详细讨论了该参数。一般是以空格分隔、区分大小写的字符串列表,例如“read write”。该参数具体需要参阅授权服务方提供的用户文档。
  5. 客户端认证信息:必须参数。在密码模式的令牌请求中必须携带客户端认证相关的信息。一般的都使用RFC 2617定义的HTTP BASIC身份认证的方式,这种技术需要客户端发送带有“Authorization”头信息的HTTP请求,Authorization头信息由三部分组成——固定单词“Basic”,后面紧跟一个空格,最后一部分是对client_id、冒号(英文)、client_secret组合而成的字符串(即client_id:client_secret)进行Base64编码后的结果,下面是示例的令牌请求中Authorization请求头的内容:
 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
另外一种方式是直接将客户端ID(client_id)和客户端密钥(client_secret)作为令牌请求URL的一部分。这具体取决于授权服务器的实现,需要参考授权服务供应商提供的文档。在我们后面实现的授权服务器中,我们将支持多种客户端认证的方式。

成功响应

当授权服务器的令牌端点接收到来自客户端的令牌请求,授权服务器将验证以下内容(我们在授权服务器实现的章节还会详细向你介绍这些内容):
  1. 令牌请求参数的基础校验(是否包含必需的参数、是否重复传参或参数值是否合法)。
  2. 认证客户端信息(客户端ID和客户端密钥)的合法性。
  3. 验证终端用户的身份凭证信息(username和password)。
在校验令牌请求的有效性和合法性后,将向客户端返回成功的响应。下面是一个成功响应的示例:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
}

  • access_token:必须参数。这是最重要的参数,客户端最终代表终端用户访问终端用户资源的“钥匙”。
  • token_type:必须参数。授权服务器颁发的令牌的类型。一般该值是“bearer”。如果你对Bearer Token的内容感兴趣,你可以详细阅读RFC 6750文档(https://datatracker.ietf.org/doc/html/rfc6750)。
  • expires_in:可选参数。以秒为单位的访问令牌的生存期。该参数为非必需参数,如果缺省这个参数,授权服务器必需以其他方式告知客户端令牌的剩余生命周期。
  • refresh_token:可选参数,客户端使用它来续订过期的访问令牌。我在第(11)小节会向你介绍这些内容的细节。
  • 其它可能出现在令牌响应中的参数,比如expires_in、scope、state参数,它们的含义我已经反复向你介绍过,你可以参阅之前的内容,在此不再赘述。

失败响应

如果令牌请求因任何原因失败,服务器将返回HTTP 400(错误请求)的状态码,并包含以下参数:
  1. error:error参数是必须的,代表令牌请求错误的原因。用来辅助开发者来定位问题,error通常是以下错误码之一:
    1. invalid_request:令牌请求缺少必要的参数,包含不支持的参数值、重复参数、包含多个凭据、使用多种机制对客户端进行身份验证,或者格式不正确。
    2. invalid_client:由于某些原因客户端身份认证失败(例如,未知客户端、没有客户端身份认证信息或不支持的身份认证方法)。
    3. invalid_grant:提供的授权类型或刷新令牌无效、过期、已吊销等,或者与授权请求中使用的重定向URI不匹配。
    4. unauthorized_client:经过身份验证的客户端无权使用这种授权类型。
    5. unsupported_grant_type:授权服务器不支持这种授权类型。
    6. invalid_scope:请求的作用域无效、未知、格式不正确或超出了资源所有者授予的作用域。
  2. error_description:(可选)人类可读的有关错误的附加信息。
  3. error_uri:(可选)有关描述错误详细信息的网页地址。
所有的参数都包含在Content-Type是application/json的HTTP响应体中。下面是一个失败响应的示例:
 HTTP/1.1 400 Bad Request
 Content-Type: application/json;charset=UTF-8
 Cache-Control: no-store
 Pragma: no-cache

{
"error":"invalid_request",
"error_description":"非法的重定向URI",
"error_uri":"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#redirection-endpoint"
}

总结

在今天的小节里我向你介绍了已经被IETF OAuth工作组明确标记为废弃状态的两种授权模式,我向你介绍这两种模式的原因在于虽然这两种模式退出历史的舞台是大势所趋,但你可能仍然在生产中使用这两种模式,并且在稍后我们自定义实现的授权服务器仍然会支持这两种模式。截止到目前为止,我已经向你描述了OAuth 2.0定义的四种主要授权类型,但是你在实践中如何选择和使用这些授权类型呢?下面我以一张表格做以总结。
 
posted @ 2023-05-07 11:29  小米粥|  阅读(56)  评论(0)    收藏  举报