# OAuth 2.0授权模型

## 简介

OAuth2.0是常用版本，允许第三方网站在用户授权的前提下访问用户在服务商那里存储的各种信息。而这种授权无须将用户的用户名和密码提供给第三方网站。OAuth允许用户提供一个令牌给第三方网站，一个令牌对应一个特定的第三方网站，同时该令牌只能在特定的时间内访问特定的资源。

传统的客户端-服务器端授权模型中，客户端请求访问受限的资源（被保护资源），资源拥有者需要分享自己的安全凭据给第三方，这会带来以下问题：

* 第三方应用要保存资源拥有者的安全凭据方便以后使用，通常是明文的用户名和密码；
* 服务端要能够支持密码授权，不论密码带来的安全隐患；
* 第三方获得过度的权限，资源拥有者无法限制有效期或者能够访问的资源范围；
* 资源拥有者无法回收某个第三方的权限并且不影响其他的第三方，而是必须更改所有第三方的密码；
* 任何第三方应用被攻陷会导致密码泄漏，进而导致密码可访问的所有数据被泄漏。

OAuth通过引入授权层来解决这些问题，将客户角色和资源拥有者的角色分开。

## SAML 2.0 and OAuth 2.0 <a href="#customermanagedapps-saml2-oauth2" id="customermanagedapps-saml2-oauth2"></a>

### SAML 2.0 <a href="#samlfederationconcept" id="samlfederationconcept"></a>

SAML 2.0 是一种行业标准，用于在 SAML 管理机构（称为身份提供商或 IdP）和 SAML 2.0 消费者（称为服务提供商或 SP）之间安全交换 SAML 断言，传递有关用户的信息。IAM Identity Center 使用这些信息为授权使用 AWS 访问门户内应用程序的用户提供联合单点登录访问。

### OAuth 2.0 <a href="#oidc-concept" id="oidc-concept"></a>

OAuth 2.0 是一种允许应用程序安全访问和共享用户数据而**无需共享密码**的协议。该功能为用户提供了一种安全、标准化的方式，允许应用程序访问其资源。OAuth 2.0 授权的基本流程包括用户、应用程序，也称为客户端、授权服务器和资源服务器。

## OAuth 2.0 流程

传统的应用授权访问是共享密码的方式，不仅不方便回收权限，并且密码会明文长期保存在第三方/客户端。

为避免共享用户密码，可信身份传播使用令牌。令牌为受信任的应用程序提供了一种标准方式，用于确认用户身份以及两个应用程序之间允许的请求。与可信身份传播集成的 AWS 受管应用程序直接从 IAM Identity Center 获取令牌。IAM Identity Center 还为应用程序提供了一个选项，用于交换来自外部 OAuth 2.0 授权服务器的身份令牌和访问令牌。这样，应用程序就可以在 AWS 外部进行身份验证并获取令牌，将令牌交换为 IAM Identity Center 令牌，并使用新令牌向 AWS 服务发出请求。如需了解更多信息，请参阅[使用受信任令牌签发器的应用程序](https://docs.aws.amazon.com/singlesignon/latest/userguide/using-apps-with-trusted-token-issuer.html)。

OAuth 2.0 流程从用户登录应用程序时开始。用户登录的应用程序发起访问其他应用程序资源的请求。发起（请求）请求的应用程序可通过向授权服务器请求令牌，代表用户访问接收应用程序。授权服务器返回令牌，发起请求的应用程序将该令牌连同访问请求传递给接收请求的应用程序。

比如，终端用户（资源拥有者）可以授权打印机访问她的保存在照片共享服务上的照片，不需要共享她的打印机的用户名和密码，而是，直接授权服务器信任她的照片共享服务，颁发给打印机专门的安全凭据（access token）。

<img src="https://3752289132-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LTfg4e0NhLEbwi6WFJp%2Fuploads%2FVNUdUl51KFiAgmDEH3fQ%2Ffile.excalidraw.svg?alt=media&#x26;token=15978e2b-7033-47dd-aacf-6b1489c8a149" alt="图1. 抽象协议流程" class="gitbook-drawing">

上图演示了OAuth 2.0的4个角色之间交互的流程，并且包含以下步骤：

（A）客户端向资源拥有者申请授权，可以直接跟资源拥有者申请，最好是通过服务器间接申请；

（B）客户端收到授权grant，也就是一个安全凭据代表资源拥有者的授权。

（C）客户端通过与授权服务器进行身份验证并提交授权许可，来申请访问令牌。授权服务器进行身份验证，并提供授权许可Access Key。

（D）授权服务器会对客户端进行身份验证，并验证授权许可。如果有效，则发出访问令牌。

（E）客户端向资源服务器请求受保护的资源，并通过出示访问令牌进行身份验证。

（F）资源服务器验证访问令牌，如果有效，则允许请求。

## 4种授权模式

在应用OAuth 2.0之前，必须在授权服务器中注册应用。平台要求开发者提供如下授权设置项。

(1)应用唯一ID(client\_id)，在服务器中唯一存在的分配给一个应用的ID，是一串公开透明的字符串，授权服务器使用该字符串来标识应用程序，并且用于构建呈现给用户的授权URL。

(2)应用密钥(client\_secret)，用于验证应用身份，并且必须在客户端和服务器之间保持私有性。一般在临时授权码换取令牌时使用。

(3)重定向URI(redirect\_uri)或回调URL(callback\_url)。重定向URI （Uniform Resource Identifier，统一资源标识符）是授权服务器在用户授权（或拒绝）应用程序之后重定向供用户访问的地址，因此也是用于处理授权码或访问令牌的应用程序的一部分。

无论哪种模式，都是为了从授权服务器获取访问令牌，用来访问资源服务器。而申请访问令牌，需要提交相应信息，如<mark style="color:blue;">client\_id（我是谁）</mark>、<mark style="color:blue;">response\_type</mark>或<mark style="color:blue;">grant\_type（申请哪种模式）</mark>、<mark style="color:blue;">scope（申请哪些权限，由授权服务器定义）</mark>、<mark style="color:blue;">redirect\_uri（申请结果跳转至哪儿）</mark>、<mark style="color:blue;">state（自定义信息希望服务器原样返回）</mark>等。当然不同的模式，提交的信息内容不同，交互的步骤也不同。下面详细描述4种授权模式的交互步骤。

### 1-授权码模式(Authorization Code)

授权码模式是功能最完整、流程最严密的授权模式，也是最烦琐、最安全的授权模式。它的特点是通过客户端的后台服务器，与服务提供商的授权服务器进行互动。

这种模式一般用于客户端是Web服务器应用或第三方的原生App调用资源服务的时候。因为在这种模式下访问令牌不会经过浏览器或移动端的App，而是直接从服务器去交换，这样就最大限度地减小了访问令牌泄露的风险。

认证流程如图所示

{% @mermaid/diagram content="sequenceDiagram
APP->>授权服务器:<http://server/oauth2/authorize> 请求认证
授权服务器->>授权服务器:检查Session用户登录状态
授权服务器-->>APP:返回登录页面
APP->>授权服务器:用户名授权登录
授权服务器->>OAuth2.0协议:认证信息
OAuth2.0协议->>OAuth2.0协议:生成临时授权码
OAuth2.0协议-->>授权服务器: callback
授权服务器-->>APP:跳转应用回调地址
APP->>APP:获取授权码
APP->>授权服务器:换取访问令牌 <http://server/server/oauth2/accesstoken>
授权服务器->>OAuth2.0协议:client\_id, redirect\_uri, client\_secret,code
OAuth2.0协议->>OAuth2.0协议:验证认证信息
OAuth2.0协议->>OAuth2.0协议:生成访问令牌
OAuth2.0协议-->>授权服务器: 返回访问令牌
授权服务器-->>APP:返回访问令牌
APP->>授权服务器:获取用户信息
授权服务器->>OAuth2.0协议:获取用户信息
OAuth2.0协议->>OAuth2.0协议:验证访问令牌
OAuth2.0协议->>OAuth2.0协议:生成用户信息
OAuth2.0协议-->>授权服务器: 返回
授权服务器-->>APP:返回
" %}

第三方应用开始进行认证时，浏览器调用如下URI。http：//[www.server.com/oauth2.0/authorize?response\_type=code\&client\_id={client\_id}\&redirect\_uri={redirect\_uri}\&scope={scope}\&state={state}其参数说明如表所示。](http://www.server.com/oauth2.0/authorize?response_type=code\&client_id={client_id}\&redirect_uri={redirect_uri}\&scope={scope}\&state={state}其参数说明如表所示。)

<table><thead><tr><th width="173">参数</th><th width="139">是否必须</th><th>说明</th></tr></thead><tbody><tr><td>response_type</td><td>是</td><td>token：请求的响应中有令牌；<br>code: 请求的响应中有授权码；<br>code_and_token: 同时请求两种</td></tr><tr><td>client_id</td><td>是</td><td>自定义的客户端标识符</td></tr><tr><td>redirect_uri</td><td>是</td><td>客户端通过User-Agent使用HTTP重定向响应，或者其他可用的方式，将终端用户引导到构建好的URI上。对于终端用户授权端点，授权服务器必须支持HTTP的GET方法，也可以支持POST方法。正确响应时，重定向URI格式如“redirect_uri?code=CODE&#x26;state=STATE”，返回参数说明如下。<mark style="color:blue;">code：</mark>由授权服务器产生的授权码。授权码应该在分发后迅速过期，以降低泄露风险。客户端一定不能重用同一个授权码。如果一个授权码被多次使用，授权服务器可能撤销之前基于这个授权码分发的所有令牌。授权码与客户端标识符和重定向URI相绑定。<mark style="color:blue;">state：</mark>如果state参数在客户端授权请求中存在，则这个参数是必需的，需要精确地设置成从客户端接收到的值。</td></tr><tr><td>scope</td><td>否</td><td>访问请求的作用域，如果有多个值，用空格区分，它们的顺序不分先后。</td></tr><tr><td>state</td><td>否</td><td>如果state参数在客户端授权请求中存在，则这个参数是必需的，需要精确地设置成从客户端接收到的值。</td></tr></tbody></table>

3返回授权码

客户端通过User-Agent使用HTTP重定向响应，或者其他可用的方式，将终端用户引导到构建好的URI上。对于终端用户授权端点，授权服务器必须支持HTTP的GET方法，也可以支持POST方法。正确响应时，重定向URI格式如“redirect\_uri?code=CODE\&state=STATE”，返回参数说明如下。

* **code：**&#x7531;授权服务器产生的授权码。授权码应该在分发后迅速过期，以降低泄露风险。<mark style="color:red;">客户端一定不能重用同一个授权码。</mark>如果一个授权码被多次使用，授权服务器可能撤销之前基于这个授权码分发的所有令牌。授权码与客户端标识符和重定向URI相绑定。
* **state：**&#x5982;果state参数在客户端授权请求中存在，则这个参数是必需的，需要精确地设置成从客户端接收到的值。错误响应时，如果终端用户拒绝了访问请求，或者由于除了缺少或无效重定向URI的其他原因而导致请求失败，会返回错误码(error)，错误码说明如下。
  * **error\_description：**&#x53EF;选参数，提供一段额外信息，用来帮助终端用户了解发生的错误。
  * **error\_uri：**&#x53EF;选参数，指定一个网页URI，其中包含关于错误的信息，用来帮助终端用户了解发生的错误。

#### 通过授权码获取访问令牌

<http://www.server.com/oauth2.0/accessToken?client\\_id={client\\_id}\\&client\\_secret={client\\_secret}\\&code={code}\\&grant\\_type=authorization\\_code\\&redirect\\_uri={redirect\\_uri}其参数说明如所示。>

<table><thead><tr><th width="173">参数</th><th width="139">是否必须</th><th>说明</th></tr></thead><tbody><tr><td>client_id</td><td>是</td><td>自定义的客户端标识符</td></tr><tr><td>client_secret</td><td>是</td><td>包含客户端私有证书</td></tr><tr><td>grant_type</td><td>是</td><td>在请求中所包含的访问许可类型。它的值必须是“authorization_code”</td></tr><tr><td>code</td><td>是</td><td>从授权服务器接收到的授权码</td></tr><tr><td>redirect_uri</td><td>是</td><td>在最初请求中使用的重定向URI</td></tr></tbody></table>

#### 获取访问令牌返回说明

客户端通过User-Agent使用HTTP重定向响应，或者其他可用的方式，将终端用户引导到构建好的URI上。对于终端用户授权端点，授权服务器必须支持HTTP的GET方法，也可以支持POST方法。正确响应时，返回参数说明如下。

* **access\_token：**&#x7531;授权服务器分发的访问令牌。
* **token\_type：**&#x5206;发的访问令牌类型。访问令牌类型告诉客户端一个信息，即当访问一个受保护资源时访问令牌应该如何被使用。
* **expires\_in：**&#x8BBF;问令牌生命周期的秒数。例如，“3600”表示自响应被授权服务器产生的时刻起，访问令牌将在一小时后过期。

错误响应时，如果终端用户拒绝了访问请求，或者由于除了缺少或无效重定向URI的其他原因而导致请求失败，会返回错误码，错误码说明如下。

**invalid\_request：**&#x8BF7;求缺少某个必需参数；包含一个不支持的参数或参数值；参数重复；包含多个私有证书；使用了多种验证客户端的机制；请求格式不正确。

**invalid\_client：**&#x63D0;供的客户端标识符是无效的；客户端验证失败；客户端不包含私有证书；提供了多个客户端私有证书；使用了不支持的证书类型。

**unauthorized\_client：**&#x7ECF;过验证的客户端没有权限使用提供的访问许可类型。

**invalid\_grant：**&#x63D0;供的访问许可是无效的、过期的或已撤销的（如无效的断言、过期的授权令牌、错误的终端用户密码证书或不匹配的授权码和重定向URI）。

**unsupported\_grant\_type：**&#x5305;含的访问许可（类型或其他属性）不被授权服务器所支持。

**invalid\_scope：**&#x8BF7;求的作用域是无效的、未知的、格式不正确的或超出了之前许可的作用域。

**error\_description：**&#x53EF;选参数，提供一段额外信息，用来帮助终端用户了解发生的错误。

**error\_uri：**&#x53EF;选参数，指定一个网页URI，其中包含关于错误的信息，用来帮助终端用户了解发生的错误。

### 2-授权码简化模式(Implicit)

在授权码模式中，授权码和访问令牌都由授权服务器生成和验证，但最终只用到访问令牌，这让授权码显得无足轻重。因此，授权码简化模式去掉了授权码的申请流程，直接通过User-Agent申请访问令牌。

{% hint style="warning" %} <mark style="color:red;">最容易受安全攻击，因为访问令牌在前端非常容易泄露。适合APP和用户在同一个设备上时使用。</mark>
{% endhint %}

认证流程：

{% @mermaid/diagram content="sequenceDiagram
APP->>授权服务器:<http://server/oauth2/authorize> 请求认证
授权服务器->>授权服务器:检查Session用户登录状态
授权服务器-->>APP:返回登录页面
APP->>授权服务器:用户名授权登录
授权服务器->>OAuth2.0协议:认证配置信息
OAuth2.0协议->>OAuth2.0协议:生成访问令牌
OAuth2.0协议-->>授权服务器: callback
授权服务器-->>APP:跳转到应用回调地址，并返回访问令牌" %}

**认证说明**

第三方应用开始进行认证时，浏览器调用如下URI。

{% code overflow="wrap" %}

```http
http://www.server.com/oauth2.0/authorize?response_type=token&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state}
```

{% endcode %}

参数说明见前面的表。

**认证返回说明**

客户端通过User-Agent使用HTTP重定向响应，或者其他可用的方式，将终端用户引导到构建好的URI上。对于终端用户授权端点，授权服务器必须支持HTTP的GET方法，也可以支持POST方法。

正确响应时，重定向URI格式如“redirect\_uri?access\_token=2YotnFZFEjr1zCsic MWpAA\&state=xyz\&token\_type=example\&expires\_in=3600”，返回参数说明如下。

* access\_token：由授权服务器分发的访问令牌。
* state：如果state参数在客户端授权请求中存在，则这个参数是必需的，需要精确地设置成从客户端接收到的值。
* token\_type：分发的访问令牌类型。访问令牌类型告诉客户端一个信息，即当访问一个受保护资源时访问令牌应该如何被使用。
* expires\_in：访问令牌生命周期的秒数。例如，“3600”表示自响应被授权服务器产生的时刻起，访问令牌将在一小时后过期。

错误响应时，如果终端用户拒绝了访问请求，或者由于除了缺少或无效重定向URI的其他原因而导致请求失败，会返回错误码，错误码说明如下。

* error\_description：可选参数，提供一段额外信息，用来帮助终端用户了解发生的错误。
* error\_uri：可选参数，指定一个网页URI，其中包含关于错误的信息，用来帮助终端用户了解发生的错误。

### 3-密码模式(Resource Owner Password Credentials)

密码模式是用户将自己的用户名和密码交给客户端，客户端用用户的用户名和密码直接换取访问令牌。

{% hint style="warning" %} <mark style="color:red;">密码模式简单，但是却意味着直接将用户的敏感信息泄露给了客户端，因此这种模式只能用于客户端是自己开发的情况下。</mark>
{% endhint %}

#### 认证流程

第一步，用户将认证用户名和密码发送给客户端。

第二步，客户端用用户的认证用户名和密码向授权服务器请求访问令牌。

第三步，授权服务器将访问令牌和刷新令牌发送给客户端。

{% @mermaid/diagram content="flowchart LR
用户 --1 发起认证--> 客户端
客户端 --2 客户端请求认证--> 授权服务器
授权服务器 --3 服务器返回访问令牌--> 客户端" %}

第三方应用开始进行认证时，通过POST方法调用如下URI。

{% code overflow="wrap" %}

```html
http://www.server.com/oauth2.0/authorize?client_id=XXXX&grant_type=password&username=user&password=pwd
```

{% endcode %}

* grant\_type：值恒定为password
* client\_id：客户端标识符
* password：用户认证的密码
* username：用户认证的用户名

### 4-客户端模式(Client Credentials)

这是一种最简单的模式，只要客户端请求就将访问令牌发送给客户端。这种模式方便但不安全，因此要求用户对客户端完全的信任，在认证过程中不需要用户的参与。

①认证流程。

第一步，客户端向授权服务器发送自己的身份信息，并请求访问令牌。

第二步，授权服务器确认客户端信息无误后，将访问令牌发送给客户端。

认证流程如图所示。

{% @mermaid/diagram content="flowchart LR
客户端 --1 客户端请求认证--> 授权服务器
授权服务器 --2 服务器返回访问令牌--> 客户端" %}

②认证说明。

第三方应用开始进行认证时，通过POST调用如下URI。

{% code overflow="wrap" %}

```html
http://www.server.com/oauth2.0/authorize?client_id=XXXX&grant_type=client_credentials
```

{% endcode %}

③认证返回说明。与密码模式的认证返回说明一致，这里不再赘述。

④应用场景。客户端模式适用于服务器间通信的场景。只有后端服务间直接通信时才需要对通信进行验证，可以使用客户凭证获取一个访问令牌，因为客户凭证可以使用对称或非对称进行加密，该方式支持共享密码或证书。

## 安全规范

OAuth 2.0协议是目前运用最广泛的一个SSO协议，在早期曾出现过不少安全方面的漏洞，其实仔细分析后会发现大多是没有严格遵循OAuth 2.0的安全指导造成的。OAuth 2.0在设计之初已经做了很多安全方面的考虑，并且在RFC6749中加入了一些安全方面的规范指导。为了进一步增强OAuth 2.0的安全性，还可以采取以下措施。

### 1-防止回调域名欺骗

服务器必须验证client\_id注册的应用与redirect\_uri是对应的，否则redirect\_uri会被伪造成第三方欺诈域名，导致服务器返回授权码而泄露。

服务器生成的临时授权码必须是一次有效，客户端使用一次后立即失效并且有效期很短，一般为30秒，由于授权码是通过redirect\_uri浏览器回调传输容易被截取，因此要保证临时授权码被客户端正常使用后不会被再次使用。

### 2-防止跨站脚本跨域攻击

例如，构造一个认证请求“redirect\_uri =<http://app.com/test?callback=”，服务器需要对redirect\\_uri进行检查，禁止输入特殊字符，并且对redirect\\_uri进行全匹配以杜绝跨站脚本攻击。>

### 3-防止跨站请求伪造

认证请求URI中的state参数是最容易被忽略的，大部分身份提供者不会强制要求客户端使用state参数。

客户端每次请求会生成唯一字符串放在state参数中，服务器认证成功返回的授权码会带上state参数，客户端验证state是否是自己生成的唯一字符串，可以确定这次请求是由客户端发出的，而不是黑客伪造的。

### 4-防止敏感信息泄露

客户端密钥、访问令牌、刷新令牌、授权码等敏感信息的安全存储。由于访问令牌是通过HTTP协议从服务器传输给客户端，为了防止旁路监听泄露访问令牌，服务器必须提供HTTPS来保证传输通道的安全性。

客户端要获取访问令牌，应该在后台与服务器交互获取，不允许访问令牌传给前端直接使用，需要保证访问令牌信息的不可猜测性，以防止被猜测得到。

### 5-令牌有效性防护

维持刷新令牌和第三方应用的绑定，刷新失效机制的设计不允许长期有效的令牌存在；维持授权码和第三方应用的绑定；对颁发出去的令牌权限进行限制，不同用户申请的令牌根据人员所属组织、角色、岗位进行数据隔离；对颁发后的令牌进行生命周期管理，可以按策略主动注销颁发的令牌。

### 6-登录过程安全性增强

具有丰富的登录认证方式，支持静态密码、手机验证码、动态密码、生物特征识别、FIDO等。

## 参考资料

[The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)

\[AWS Blog] [Simplify workforce identity management using IAM Identity Center and trusted token issuers](https://aws.amazon.com/blogs/security/simplify-workforce-identity-management-using-iam-identity-center-and-trusted-token-issuers/)

<https://docs.aws.amazon.com/singlesignon/latest/userguide/customermanagedapps-saml2-oauth2.html>

[Trusted identity propagation overview](https://docs.aws.amazon.com/singlesignon/latest/userguide/trustedidentitypropagation-overview.html)

\[AWS Blog] [How to use OAuth 2.0 in Amazon Cognito: Learn about the different OAuth 2.0 grants](https://aws.amazon.com/blogs/security/how-to-use-oauth-2-0-in-amazon-cognito-learn-about-the-different-oauth-2-0-grants/)
