Laravel 5.6 - 用于自助API的Passport JWT httponly cookie SPA身份验证?

问题描述 投票:48回答:4

注意:我在这个问题上有4个赏金,但下面没有提出的答案是这个问题所需要的答案。所需的一切都在下面的Update 3中,只是寻找要实现的Laravel代码。


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

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

enter image description here


我已经做了很多研究,但是当使用Laravel和JWT httponly cookie进行自我消费的API时,信息总是很短而且不完整(大多数在线教程只显示JWT存储在本地存储中并且不是很安全)。看起来像包含JWT by Passport的httponly cookie应该用于识别Javascript端的用户,当每次请求发送到服务器以验证用户是否是他们所说的用户时。

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

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

我希望这个问题的答案可以作为一个易于遵循的指南,为未来的读者和那些正在努力寻找一个自我消费API的上述观点的答案。

更新1:

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

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

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

  1. 在客户端,似乎Authorization: Bearer <token>不是处理安全的httpOnly cookie时的方法。我认为请求/响应应该包含安全的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:

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

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

刷新令牌授权:当服务器发出访问令牌时,它还会为访问令牌设置到期时间。当我们想要在访问令牌过期时刷新访问令牌时,使用刷新令牌授予。在这种情况下,授权服务器将在发出访问令牌时发送刷新令牌,该令牌可用于请求新的访问令牌。

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

laravel laravel-5 csrf-protection laravel-passport cookie-httponly
4个回答
10
投票

Laravel Passport 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. 这里解释设置auth路线(https://laravel.com/docs/5.5/authentication
  2. 这里解释了设置护照(https://laravel.com/docs/5.5/passport)。

重要的部分是:

  • Laravel\Passport\HasApiTokens特性添加到您的User模型中
  • driver身份验证后卫的api选项设置为passport中的config/auth.php
  • \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,中间件添加到webapp/Http/Kernel.php中间件组

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

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

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

axios.get(
  '/login, 
  {
    email: '[email protected]',
    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,email ...)生成令牌(此令牌不保存在任何地方),然后服务器返回带有加密cookie的响应,该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。祝新年快乐!! :)


6
投票
  • Laravel Passport是PHP League的OAuth服务器的一个实现
  • 密码授予类型可用于用户名+密码验证
  • 请记住通过在代理中发出身份验证请求来隐藏您的客户端凭据
  • 将刷新令牌保存在HttpOnly cookie中,以最大限度地降低XSS攻击的风险

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

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


6
投票

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

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

如果您对以上几点有所了解,请告诉我。


3
投票

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

我应该使用哪种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-storePragma: no-cacheExpires: 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(将在下面详细说明)

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

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

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

为什么我不应该在浏览器localstorage或sessionstorage中存储令牌?

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

  1. 如果发生XSS,恶意脚本可以从那里轻松读取令牌并将其发送到远程服务器。有远程服务器或攻击者在冒充受害用户时没有问题。
  2. localstorage和sessionstorage不在子域之间共享。因此,如果我们在不同的子域上运行两个SPA,我们将无法获得SSO功能,因为一个应用程序存储的令牌将无法供组织内的其他应用程序使用

但是,如果令牌仍存储在任何这些浏览器存储中,则必须包含正确的指纹。指纹是一种加密强大的随机字节串。然后,原始字符串的Base64字符串将存储在名称前缀为HttpOnlySecureSameSite__Secure- cookie中。 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是论坛,则经过身份验证的用户无法关注该链接。如果这是对应用程序的严重限制,只要HTTP方法是安全的,就可以使用SameSite=lax,这将允许跨站点请求。 GET,HEAD,OPTIONS和TRACE。由于CSRF基于不安全的方法,如POST,PUT,DELETE,lax仍然提供针对CSRF的保护
  2. 要允许cookie在所有请求中传递到“example.com”的任何子域,cookie的域属性应设置为“example.com”

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

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

如何验证令牌?

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

但是,对于资源服务器上的每个API调用,授权服务器都不涉及验证令牌。资源服务器可以缓存授权服务器发布的令牌。资源服务器可以使用内存数据网格(即Redis),或者如果一切都不能存储在RAM中,则可以使用基于LSM的DB(即具有级别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 documentation中所述,服务器必须发送包含唯一不可预测的非HttpOnly cookie(换句话说是可读cookie)该特定会话的值。它应该是加密强大的随机值。然后客户端将始终读取cookie并在自定义HTTP头中发送值(GET和HEAD请求除外,这些请求不应具有任何状态更改逻辑。由于相同的原始策略,CSRF无法从目标Web应用程序读取任何内容),因此服务器可以验证标头和cookie中的值。由于跨域表单无法读取cookie或设置自定义标头,以防CSRF请求,自定义标头值将丢失,服务器将能够检测到攻击
  2. 要保护应用程序不要登录CSRF,请始终检查referer标头并仅在referer是受信任域时接受请求。如果缺少referer标头或非白名单域,则只需拒绝该请求。当使用SSL / TLS时,通常会出现referrer。着陆页(主要是信息性的,不包含登录表单或任何安全内容)可能会有点放松,并允许缺少referer标头的请求
  3. 应该在服务器中阻止TRACE HTTP方法,因为这可以用来读取httpOnly cookie
  4. 此外,将标头Strict-Transport-Security: max-age=<expire-time>; includeSubDomains设置为仅允许安全连接,以防止任何中间人从子域覆盖CSRF cookie
  5. 另外,应该使用如上所述的SameSite设置

最后,SSL / TLS对于所有通信都是必需的 - 如今,低于1.1的TLS版本不符合PCI / DSS要求。应使用适当的密码套件来确保前向保密和经过身份验证的加密。此外,只要用户明确点击“注销”以防止任何令牌误用的可能性,就应将访问和刷新令牌列入黑名单。

参考

  1. RFC 6749 - OAuth2.0
  2. OWASP JWT Cheat Sheet
  3. SameSite Cookie IETF Draft
  4. Cookie Prefixes
  5. RFC 6265 - Cookie
© www.soinside.com 2019 - 2024. All rights reserved.