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

时间:2018-12-07 23:26:03

标签: laravel laravel-5 csrf-protection laravel-passport cookie-httponly

注意:我在这个问题上有4个赏金,但是以下未获好评的答案都不是该问题的答案。所需的一切都在下面的Update 3中,只需寻找要实现的Laravel代码即可。


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

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

enter image description here


我已经做了很多研究,但是在将Laravel与JWT httponly cookie用于自用API时,信息总是简短而不完整(大多数在线教程仅显示JWT存储在本地存储中,即不太安全)。看起来,当通过向服务器发送的每个请求来验证用户身份时,应该使用包含Passport JWT的httponly cookie来识别Javascript端的用户。

要了解如何使此设置有效的完整图片,还需要做一些其他事情,而在涵盖该内容的单个教程中没有见过:

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

我希望这个问题的答案能为将来的读者和那些正在努力寻找自述API涵盖以上各点的读者提供一个容易遵循的指南。

更新1:

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

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

因此,似乎可以满足第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:

密码授予/刷新令牌授予组合似乎是必经之路。使用 Password Grant / Refresh Token Grant 组合寻找易于遵循的实施指南。

  

密码授予:   此赠款适合与我们信任的客户打交道,   就像我们自己网站的移动应用一样。在这种情况下,客户端发送   用户登录到授权服务器的凭据以及   服务器直接颁发访问令牌。

     

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

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

4 个答案:

答案 0 :(得分:10)

Laravel Passport JWT

  1. 要使用此功能,您需要禁用cookie序列化。 Laravel 5.5在cookie值的序列化/反序列化方面存在问题。 您可以在这里(https://laravel.com/docs/5.5/upgrade

  2. 了解更多信息
  3. 请确保

    • 刀片模板头中有<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中将api身份验证保护的passport选项设置为config/auth.php
  • \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,中间件添加到web中的app/Http/Kernel.php中间件组中

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

  1. /login发出POST请求,以传递您的凭据。您可以提出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。祝新年快乐!! :)

答案 1 :(得分:6)

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

更多信息,请点击此处

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

答案 2 :(得分:6)

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

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

让我知道您是否清楚上述几点。

答案 3 :(得分:3)

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

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

这是要决定的第一件事。对于SPA,两个可能的选项是:

  1. 授权码授予(建议,如果客户端机密存储在服务器端,则建议使用)
  2. 资源所有者密码凭据授予

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

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

(“客户端凭据”授予类型不在此讨论范围之内,因为在客户端不代表用户执行操作时使用该凭据。例如,批处理作业)

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

(在授权码授予类型中)该流程如下所示:

  1. 用户单击SPA登录页面上的登录按钮
  2. 用户将被重定向到授权服务器登录页面。 URL查询参数中提供了客户端ID
  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还应该具有适当的过滤器,以验证访问令牌。

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

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

  1. 如果发生XSS,则恶意脚本可以轻松地从那里读取令牌并将其发送到远程服务器。从此以后,远程服务器或攻击者将不会冒充受害用户。

  2. 本地存储和会话存储未在子域之间共享。因此,如果我们有两个运行在不同子域上的SPA,我们将无法获得SSO功能,因为一个应用程序存储的令牌对于组织内的另一个应用程序将不可用

但是,如果令牌仍存储在任何这些浏览器存储中,则必须包括正确的指纹。指纹是加密强度高的随机字节串。然后,原始字符串的Base64字符串将存储在名称前缀为HttpOnly的{​​{1}},SecureSameSite cookie中。 __Secure-Domain属性的正确值。字符串的SHA256哈希也将在JWT声明中传递。因此,即使XSS攻击将JWT访问令牌发送到受攻击者控制的远程服务器,它也无法发送cookie中的原始字符串,因此服务器可以基于cookie的缺失拒绝请求。同样,可以通过使用适当的Path响应标头来进一步缓解XSS和脚本注入。

注意:

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

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

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

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

如何验证令牌?

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

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

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

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

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

  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的影响,请始终检查Path标头并仅在referer是受信任的域时接受请求。如果没有referer标头或非列入白名单的域,只需拒绝该请求即可。使用SSL / TLS时,通常存在referer。登陆页面(主要是信息性的,不包含登录表单或任何受保护的内容,可能会稍微放松一些,并允许缺少referrer头的请求

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

  4. 也设置标题 httpOnly仅允许安全连接,以防止任何中间人覆盖子域中的CSRF Coo​​kie

  5. 此外,应使用上述Strict-Transport-Security: max-age=<expire-time>; includeSubDomains设置

最后,所有通信都必须使用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