68

注意:我在这个问题上有 4 个赏金,但下面的投票答案都不是这个问题所需的答案。所需的一切都在下面的更新 3 中,只是寻找 Laravel 代码来实现。


更新 3:这个流程图正是我想要完成的流程,下面的所有内容都是一些旧更新的原始问题。此流程图总结了所需的一切。

下面流程图中的绿色部分是我知道该怎么做的部分。红色部分及其旁注是我正在寻找使用 Laravel 代码完成的帮助。

在此处输入图像描述


我做了很多研究,但是在将 Laravel 与 JWT httponly cookie 一起用于自用 API 时,信息总是很短且不完整(大多数在线教程仅显示 JWT 存储在本地存储中,这不是很安全)。看起来应该使用包含 Passport 的 JWT 的 httponly cookie 来识别 Javascript 端的用户,当每个请求发送到服务器以验证用户是他们所说的那个人时。

还需要一些额外的东西来全面了解如何使这个设置工作,我在一个涵盖这个的教程中没有遇到过:

  1. Laravel Passport(不是 tymon auth)生成加密的 JWT 并在从 JS 端登录后将其作为 httponly cookie 作为响应发送。使用什么中间件?如果刷新令牌增加了更多安全性,如何实现?
  2. 调用 auth 端点的 JavaScript(例如 axios)api 伪代码,如何将 httponly cookie 传递给后端,以及后端如何验证令牌是否有效。
  3. 如果从多个设备登录单个帐户,则设备被盗,如何撤消所有经过身份验证的用户设备的访问权限(假设用户从他们控制的已登录设备更改密码)?
  4. 登录/注册、注销、更改密码、忘记密码控制器方法通常会如何处理令牌的创建/验证/撤销?
  5. CSRF 令牌集成。

我希望这个问题的答案可以作为一个易于理解的指南,供未来的读者和那些目前正在努力寻找涵盖上述自用 API 的答案的人使用。

更新 1:

  1. 请注意我之前尝试CreateFreshApiToken过,但是在撤销用户的令牌时不起作用(对于上面的第 3 点和第 4 点)。这是基于核心 laravel 开发人员在谈论中间件时的评论:CreateFreshApiToken

此中间件创建的 JWT 令牌不会存储在任何地方。它们不能被撤销或“不存在”。它们只是为您的 api 调用提供了一种通过 laravel_token cookie 进行身份验证的方法。它与访问令牌无关。另外:您通常不会在发布它们的同一应用程序上使用客户端发布的令牌。您可以在第一方或第三方应用程序中使用它们。要么使用中间件,要么使用客户端发出的令牌,但不能同时使用两者。

所以它似乎能够满足第 3 点和第 4 点来撤销令牌,如果使用CreateFreshApiToken中间件则不可能这样做。

  1. 在客户端,处理安全的 httpOnly cookie 似乎Authorization: Bearer <token>不是要走的路。我认为请求/响应应该包含安全的 httpOnly cookie 作为请求/响应标头,就像这样基于 laravel 文档:

当使用这种身份验证方法时,默认的 Laravel JavaScript 脚手架会指示 Axios 始终发送 X-CSRF-TOKEN 和 X-Requested-With 标头。

headerswindow.axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-TOKEN': (csrf_token goes here)
};

这也是我正在寻找涵盖上述所有要点的解决方案的原因。抱歉,我使用的是 Laravel 5.6 而不是 5.5。

更新 2:

看来密码授予/刷新令牌授予组合是要走的路。寻找使用密码授予/刷新令牌授予组合的易于遵循的实施指南。

密码授权: 此授权适用于与我们信任的客户打交道,例如我们自己网站的移动应用程序。在这种情况下,客户端将用户的登录凭据发送到授权服务器,服务器直接颁发访问令牌。

Refresh Token Grant: 当服务器发出访问令牌时,它也会设置访问令牌的到期时间。当我们想要在访问令牌过期后刷新访问令牌时,使用刷新令牌授权。在这种情况下,授权服务器会在发布访问令牌时发送一个刷新令牌,该令牌可用于请求新的访问令牌。

我正在寻找一个易于实施、直接、全面的答案,使用密码授予/刷新令牌授予组合,该组合涵盖上述原始 5 点的所有部分,使用 httpOnly 安全 cookie、创建/撤销/刷新令牌、登录 cookie 创建,注销 cookie 撤销、控制器方法、CSRF 等。

4

4 回答 4

30

我将尝试以通用的方式回答这个问题,以便答案适用于框架、实现和语言,因为所有问题的答案都可以从通用协议或算法规范中得出。

我应该使用哪种 OAuth 2.0 授权类型?

这是首先要决定的。当谈到 SPA 时,两种可能的选择是:

  1. 授权码授予(推荐,前提是客户端密码存储在服务器端)
  2. 资源所有者密码凭证授予

我没有提到隐式授权类型作为选项的原因是:

  1. 缺少通过提供客户端密码和授权代码的客户端身份验证步骤。所以安全性较低
  2. 访问令牌作为 URL 片段发送回(以便令牌不会发送到服务器),该片段将继续保留在浏览器历史记录中
  3. 如果发生 XSS 攻击,恶意脚本可以很好地将令牌发送到远程服务器以控制攻击者

(客户端凭证授权类型不在本讨论范围内,因为它在客户端不代表用户行事时使用。例如批处理作业)

在授权码授权类型的情况下,授权服务器通常是与资源服务器不同的服务器。最好将授权服务器分开,并将其用作组织内所有 SPA 的通用授权服务器。这始终是推荐的解决方案。

这里(在授权代码授权类型中)流程如下所示:

  1. 用户点击 SPA 登陆页面上的登录按钮
  2. 用户被重定向到授权服务器登录页面。客户端 ID 在 URL 查询参数中提供
  3. 用户输入他/她的凭据并单击登录按钮。用户名和密码将使用 HTTP POST 发送到授权服务器。凭据应在请求正文或标头中发送,而不是在 URL 中(因为 URL 记录在浏览器历史记录和应用程序服务器中)。此外,应设置适当的缓存 HTTP 标头,以便不缓存凭据:Cache-Control: no-cache, no-store, Pragma: no-cache,Expires: 0
  4. 授权服务器根据用户数据库(例如 LDAP 服务器)对用户进行身份验证,其中用户名和用户密码的哈希(哈希算法,如 Argon2、PBKDF2、Bcrypt 或 Scrypt)与随机盐一起存储
  5. 成功验证后,授权服务器将根据 URL 查询参数中提供的客户端 ID 从其数据库中检索重定向 URL。重定向 URL 是资源服务器 URL
  6. 然后,用户将被重定向到资源服务器端点,并在 URL 查询参数中使用授权代码
  7. 然后,资源服务器将向授权服务器发出 HTTP POST 请求以获取访问令牌。授权码、客户端 ID、客户端密码应该放在请求正文中。(应使用上述适当的缓存标头)
  8. 授权服务器将在响应正文或标头中返回访问令牌和刷新令牌(使用上述适当的缓存标头)
  9. 资源服务器现在将通过设置适当的 cookie 将用户(HTTP 响应代码 302)重定向到 SPA URL(将在下面详细说明)

另一方面,对于资源所有者密码凭证授予类型,授权服务器和资源服务器相同。它更易于实施,如果符合要求和实施时间表,也可以使用。

有关资源所有者授权类型的更多详细信息,请参阅我在此处的回答。

在此可能需要注意的是,在 SPA 中,所有受保护的路由只有在调用适当的服务以确保请求中存在有效令牌后才应启用。同样,受保护的 API 也应该有适当的过滤器来验证访问令牌。

为什么我不应该将令牌存储在浏览器本地存储或会话存储中?

许多 SPA 确实在浏览器本地存储或会话存储中存储访问和/或刷新令牌。我认为我们不应该将令牌存储在这些浏览器存储中的原因是:

  1. 如果发生 XSS,恶意脚本可以轻松地从那里读取令牌并将它们发送到远程服务器。从那里开始,远程服务器或攻击者在冒充受害者用户方面没有问题。

  2. localstorage 和 sessionStorage 不跨子域共享。因此,如果我们在不同的子域上运行两个 SPA,我们将无法获得 SSO 功能,因为一个应用程序存储的令牌将无法用于组织内的另一个应用程序

但是,如果令牌仍存储在任何这些浏览器存储中,则必须包含正确的指纹。指纹是加密的强随机字节串。然后将原始字符串的 Base64 字符串存储在一个HttpOnly, Secure,SameSite带有 name 前缀的 cookie 中__Secure-DomainPath属性的正确值。字符串的 SHA256 哈希也将在 JWT 的声明中传递。因此,即使 XSS 攻击将 JWT 访问令牌发送到攻击者控制的远程服务器,它也无法在 cookie 中发送原始字符串,因此服务器可以根据缺少 cookie 拒绝请求。此外,可以通过使用适当的content-security-policy响应标头进一步缓解 XSS 和脚本注入。

笔记:

  1. SameSite=strict确保给定的 cookie 不会伴随来自不同站点(AJAX 或通过以下超链接)的请求。简而言之 - 来自与目标站点具有相同“可注册域”的站点的任何请求都将被允许。例如,如果“http://www.example.com”是站点名称,则可注册域是“example.com”。有关详细信息,请参阅参考编号。3 在下面的最后一节中。因此,它提供了一些针对 CSRF 的保护。但是,这也意味着如果给出的 URL 是论坛,则经过身份验证的用户无法访问该链接。如果这对应用程序来说是一个严重的限制,SameSite=lax那么只要 HTTP 方法是安全的,即允许跨站点请求,就可以使用它。GET、HEAD、OPTIONS 和 TRACE。由于 CSRF 基于不安全的方法,如 POST、PUT、lax仍然提供针对 CSRF 的保护

  2. 要允许 cookie 在所有请求中传递到“example.com”的任何子域,cookie 的 domain 属性应设置为“example.com”

为什么要在 cookie 中存储访问令牌和/或刷新令牌?

  1. 在将令牌存储在 cookie 中时,我们可以将 cookie 设置为securehttpOnly。因此,如果发生 XSS,恶意脚本将无法读取并将其发送到远程服务器。XSS 仍然可以从用户的浏览器中冒充用户,但是如果浏览器关闭,脚本就不会造成进一步的破坏。secure标志确保令牌不能通过不安全的连接发送 - SSL/TLS 是强制性的
  2. 例如,将 cookie 中的根域设置为domain=example.com可确保 cookie 可跨所有子域访问。因此,组织内的不同应用程序和服务器可以使用相同的令牌。只需登录一次

如何验证令牌?

令牌通常是 JWT 令牌。通常令牌的内容不是秘密的。因此,它们通常不加密。如果需要加密(可能是因为某些敏感信息也在令牌中传递),则有一个单独的规范 JWE。即使不需要加密,我们也需要确保令牌的完整性。没有人(用户或攻击者)应该能够修改令牌。如果他们这样做了,服务器应该能够检测到并拒绝所有使用伪造令牌的请求。为了确保这种完整性,JWT 令牌使用 HmacSHA256 等算法进行数字签名。为了生成此签名,需要一个密钥。授权服务器将拥有并保护该秘密。每当调用授权服务器 api 来验证令牌时,授权服务器将在传递的令牌上重新计算 HMAC。如果它与输入 HMAC 不匹配,则返回否定响应。JWT 令牌以 Base64 编码格式返回或存储。

但是,对于资源服务器上的每个 API 调用,授权服务器都不会参与验证令牌。资源服务器可以缓存授权服务器颁发的令牌。资源服务器可以使用内存中的数据网格(即 Redis),或者,如果所有内容都无法存储在 RAM 中,则可以使用基于 LSM 的 DB(即带有 Level DB 的 Riak)来存储令牌。

对于每个 API 调用,资源服务器都会检查其缓存。

  1. 如果缓存中不存在访问令牌,API 应返回适当的响应消息和 401 响应代码,以便 SPA 可以将用户重定向到将请求用户重新登录的适当页面

  2. 如果访问令牌有效但已过期(注意,JWT 令牌通常包含用户名和到期日期等),API 应返回适当的响应消息和 401 响应代码,以便 SPA 可以调用适当的资源服务器 API使用刷新令牌(使用适当的缓存标头)更新访问令牌。然后,服务器将使用访问令牌、刷新令牌和客户端密钥调用授权服务器,并且授权服务器可以返回新的访问和刷新令牌,这些令牌最终会流向 SPA(带有适当的缓存标头)。然后客户端需要重试原始请求。所有这些都将由系统处理,无需用户干预。可以创建一个单独的 cookie 来存储类似于访问令牌但具有适当值的刷新令牌Path属性,因此刷新令牌不会伴随每个请求,但仅在续订请求中可用

  3. 如果刷新令牌无效或过期,API 应返回适当的响应消息和 401 响应代码,以便 SPA 可以将用户重定向到将请求用户重新登录的适当页面

为什么我们需要两个令牌——访问令牌和刷新令牌?

  1. 访问令牌的有效期通常很短,比如 30 分钟。刷新令牌通常有更长的有效期,比如 6 个月。如果访问令牌以某种方式被泄露,攻击者只有在访问令牌有效的情况下才能冒充受害者用户。由于攻击者没有客户端密码,因此它无法向授权服务器请求新的访问令牌。然而,攻击者可以请求资源服务器进行令牌更新(如上述设置中,更新请求通过资源服务器以避免将客户端密码存储在浏览器中),但考虑到采取的其他步骤,它不太可能,而且服务器可以根据 IP 地址采取额外的保护措施。

  2. 如果需要,访问令牌的这个短有效期有助于授权服务器撤销客户端颁发的令牌。授权服务器还可以维护已发布令牌的缓存。然后,如果需要,系统管理员可以将某些用户的令牌标记为已撤销。在访问令牌到期时,当资源服务器将转到授权服务器时,用户将被强制重新登录。

CSRF 呢?

  1. 为了保护用户免受 CSRF 的影响,我们可以遵循 Angular 等框架中遵循的方法(如 Angular HttpClient文档中所述服务器必须发送一个非 HttpOnly cookie(换句话说,一个可读 cookie),其中包含该特定会话的唯一不可预测值。它应该是一个加密的强随机值。然后,客户端将始终读取 cookie 并在自定义 HTTP 标头中发送值(GET 和 HEAD 请求除外,它们不应具有任何状态更改逻辑。请注意,由于相同的来源策略,CSRF 无法从目标 Web 应用程序读取任何内容)以便服务器可以验证来自标头和 cookie 的值。由于跨域表单无法读取cookie或设置自定义标头,因此在CSRF请求的情况下,自定义标头值将丢失,服务器将能够检测到攻击

  2. 为了保护应用程序免受登录 CSRF 的影响,请始终检查标头并仅在受信任域referer时才接受请求。referer如果referer标头不存在或未列入白名单的域,则只需拒绝请求。使用 SSL/TLS 时referrer通常存在。登陆页面(主要是信息性的,不包含登录表单或任何安全内容可能有点放松,并允许缺少referer标题的请求

  3. TRACE应该在服务器中阻止 HTTP 方法,因为这可用于读取httpOnlycookie

  4. 此外,将标头设置 Strict-Transport-Security: max-age=<expire-time>; includeSubDomains为仅允许安全连接,以防止任何中间人覆盖来自子域的 CSRF cookie

  5. 此外,SameSite应使用上述设置

  6. 状态变量(Auth0 使用它)- 客户端将生成并随每个请求传递一个加密的强随机随机数,服务器将连同其响应一起回显该随机数,从而允许客户端验证随机数。Auth0 文档中对此进行了解释

最后,SSL/TLS 对于所有通信都是强制性的——就像今天一样,低于 1.1 的 TLS 版本对于 PCI/DSS 合规性是不可接受的。应使用适当的密码套件来确保前向保密和经过身份验证的加密。此外,一旦用户明确单击“注销”,就应将访问和刷新令牌列入黑名单,以防止任何令牌滥用的可能性。

参考

  1. RFC 6749 - OAuth2.0
  2. OWASP JWT 备忘单
  3. SameSite Cookie IETF 草案
  4. Cookie 前缀
  5. RFC 6265 - Cookie
于 2019-01-02T18:51:29.973 回答
15

Laravel 护照 JWT

  1. 要使用此功能,您需要禁用 cookie 序列化。Laravel 5.5 存在 cookie 值的序列化/反序列化问题。您可以在此处阅读更多相关信息(https://laravel.com/docs/5.5/upgrade

  2. 确保

    • 你有<meta name="csrf-token" content="{{ csrf_token() }}">你的刀片模板头

    • axios 设置为在每个请求上使用 csrf_token。

你应该有这样的东西resources/assets/js/bootstrap.js

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
let token = document.head.querySelector('meta[name="csrf-token"]');

if (token) {
  window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
  console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
  1. 此处解释了设置身份验证路由(https://laravel.com/docs/5.5/authentication
  2. 设置护照在这里解释(https://laravel.com/docs/5.5/passport)。

重要的部分是:

  • Laravel\Passport\HasApiTokens特征添加到您的User模型中
  • 将身份验证保护的driver选项设置为apipassportconfig/auth.php
  • \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,中间件添加到您的web中间件组中app/Http/Kernel.php

请注意,您可能可以跳过迁移和创建客户端。

  1. 发出 POST 请求以/login传递您的凭据。您可以发出 AJAX 请求或普通表单提交。

如果登录请求是 AJAX(使用 axios),则响应数据将是 HTML,但您感兴趣的是状态码。

axios.get(
  '/login, 
  {
    email: 'user@email.com',
    password: 'secret',
  },
  {
    headers: {
      'Accept': 'application/json', // set this header to get json validation errors.
    },
  },
).then(response => {
  if (response.status === 200) {
      // the cookie was set in browser
      // the response.data will be HTML string but I don't think you are interested in that
    }
    // do something in this case
}).catch(error => {
  if (error.response.status === 422) {
    // error.response.data is an object containing validation errors
  }
  // do something in this case
});

登录时,服务器通过提供的凭据找到用户,根据用户信息(id,电子邮件...)生成令牌(此令牌未保存在任何地方)然后服务器返回带有加密 cookie 的响应,其中包含生成的令牌.

  1. 对受保护的路由进行 API 调用。

假设您有一条受保护的路线

Route::get('protected', 'SomeController@protected')->middleware('auth:api');

您可以像往常一样使用 axios 进行 ajax 调用。cookie 是自动设置的。

axios.get('/api/protected')
  .then(response => {
    // do something with the response
  }).catch(error => {
    // do something with this case of error
  });

当服务器接收到调用解密请求laravel_cookie并获取用户信息(例如:id、email ...)然后使用该用户信息进行数据库查找以检查用户是否存在。如果找到该用户,则该用户被授权访问所请求的资源。否则返回 401。

使 JWT 令牌无效。正如您提到的评论,无需担心这一点,因为此令牌未保存在服务器上的任何位置。

更新

关于第 3 点 Laravel 5.6 Auth 有一个新方法logoutOtherDevices您可以从这里 ( https://laracasts.com/series/whats-new-in-laravel-5-6/episodes/7 )了解更多信息,因为文档非常简单。

如果您无法更新您的 Laravel 版本,您可以查看 5.6 中是如何完成的,并为 5.5 构建您自己的实现

您的问题中的第 4 点。看看在app/Http/Controllers/Auth.

关于 access_tokens 和 refresh_tokens 这是一种完全不同且更复杂的方法。你可以在网上找到很多教程来解释如何做到这一点。

我希望它有所帮助。

PS。祝新年快乐!!:)

于 2018-12-31T06:58:58.040 回答
7
  • Laravel Passport 是 PHP League 的 OAuth 服务器的一个实现
  • 密码授权类型可用于用户名+密码认证
  • 请记住通过在代理中发出身份验证请求来隐藏您的客户端凭据
  • 将刷新令牌保存在 HttpOnly cookie 中,以最大程度地降低 XSS 攻击的风险

您可以在此处查看更多信息

http://esbenp.github.io/2017/03/19/modern-rest-api-laravel-part-4/

于 2018-12-11T05:21:27.800 回答
7

我还在我的项目中实施了 Laravel 护照,我想我已经涵盖了你在问题中提到的大部分要点。

  1. 我已使用密码授权来生成访问令牌和刷新令牌。您可以按照以下步骤设置护照并实施护照授予。在您的登录方法中,您必须验证用户凭据并生成令牌并将 cookie(将 cookie 附加到响应)附加到响应。如果你需要我可以给你一些例子。
  2. 我为 CORS(处理传入的请求标头)添加了两个中间件,并检查传入的访问令牌是否有效,如果无效,则从存储的刷新令牌(刷新令牌)生成访问令牌。我可以给你看例子。
  3. 登录后,来自客户端的所有请求都应包含 Authorization 标头(Authorization: Bearer <token>)。

如果您清楚以上几点,请告诉我。

于 2018-12-27T09:19:19.907 回答