在与 api 不同的域上对单页应用进行 Auth0 身份验证

Posted

技术标签:

【中文标题】在与 api 不同的域上对单页应用进行 Auth0 身份验证【英文标题】:Auth0 authentication of single-page-app on a different domain than the api 【发布时间】:2017-03-22 16:58:53 【问题描述】:

我正在尝试将 Auth0 身份验证添加到我的单页应用程序。我的应用程序在一个域下运行,例如 app.mycompany.com,而此应用程序使用的 api 在另一个域下运行,例如 api.mycompany.com。

我知道这个帖子:

Single Sign On (SSO) solution/architecture for Single Page App (SPA)

以及此处链接的 auth0 文章和 github 存储库。但我感觉我的场景稍微简单一些,因为我不一定希望在几个不同的单页应用程序之间进行单点登录。首先,我只想要 API 和应用程序之间的分离。

这是我已经尝试过的:

我已经从文章React Login With Auth0 开始并下载了入门项目。我肯定可以毫无问题地登录,它会在我的 localStorage 中留下一个 id_token,其中包含由 Auth0 发布的 JWS。

我也可以直接登录 api.mycompany.com(我的 FeathersJS API 应用程序),我可以看到在 OAuth 重定向过程中,id_token 令牌神奇地转换为我的 Feathers 应用程序发出的包含与 auth0-ID 匹配的用户对象的内部 ID。我还实现了用于从 Auth0-ID 映射到我的内部 ID 的逻辑。此外,我所有的 Feathers 钩子(例如验证令牌和用户数量)都在工作。

我想不通的是如何使用 localStorage 中的 Auth0 令牌更改 app.mycompany.com 下运行的 react-application,以便此令牌由 api.mycompany.com 转换为 feathers-jwt 令牌,这样所有后续的 API 调用都会自动包含 feathers-jwt 令牌,以便 API 可以验证用户并返回正确的数据。

任何关于如何进行的建议将不胜感激。

更多背景细节:

api基于node.js和featherjs(基本上是Express的扩展) 单页应用程序基于 ReactJS 构建,由一个简单的 Express 服务器提供服务,但它可以由任何可以通过 http 提供静态文件的服务器提供服务。单页应用程序向 api 发出 http 请求以读取数据并执行操作。

该 api 有以下代码行负责身份验证:

const authentication = require('feathers-authentication');
const Auth0Strategy = require('passport-auth0').Strategy;
app.configure(authentication(
    local:false,
    token: 
      secret: 'mysecret',
      payload: ['email', 'auth0Nickname'],
      issuer: 'mycompany'
    ,
    idField: 'id',
    shouldSetupSuccessRoute: false,
    auth0: 
      strategy: Auth0Strategy,
      domain: 'mycompany.eu.auth0.com',
      'clientID': 'xxx',
      'clientSecret': 'yyy'
    
));

【问题讨论】:

【参考方案1】:

我遇到了和你一样的问题,我想通过单页应用程序对用户进行身份验证,调用位于其他服务器上的 API。

official auth0 example 是一个经典的 Express Web 应用程序,它执行身份验证并呈现 html 页面,但它不是连接到托管在其他域上的 API 的 SPA。

让我们分解一下这个例子中用户进行身份验证时会发生什么:

用户发出请求调用/auth/auth0 route 用户被自动重定向到 Auth0 身份验证过程(Auth0 登录表单选择提供者,然后选择提供者登录屏幕) 用户被重定向到/auth/success路由 /auth/success 路由重定向到静态html页面public/success.html,同时发送一个包含用户令牌的jwt-token cookie 客户端,当public/success.html加载时,Feathers客户端authenticate()方法从cookie中读取token并保存在本地存储中。

从现在开始,Feathers 客户端将对从本地存储读取 cookie 的用户进行身份验证。

我尝试将此场景调整为单页应用程序架构,实现以下流程:

在 SPA 中,使用包含 SPA URL 的 source 查询字符串参数调用身份验证 API。例如:http://my-api.com/auth/auth0?source=http://my-spa.com 服务器端,在 /auth/auth0 路由处理程序中,创建一个 cookie 来存储该 URL 成功登录后,读取source cookie 将用户重定向回 SPA,在 cookie 中发送 JWT 令牌。

但最后一步不起作用,因为您无法在给定域(API 服务器域)上设置 cookie 并将用户重定向到其他域! (更多关于 *** 上的 here)

所以实际上我通过以下方式解决了问题:

服务器端:使用 URL 哈希将令牌发送回客户端。 客户端:创建一个新的 html 页面,从 URL 哈希中读取令牌

服务器端代码:

// Add a middleware to write in a cookie where the user comes from
// This cookie will be used later to redirect the user to the SPA
app.get('/auth/auth0', (req, res, next) => 
  const  origin  = req.query
  if (origin) 
    res.cookie(WEB_CLIENT_COOKIE, origin)
   else 
    res.clearCookie(WEB_CLIENT_COOKIE)
  
  next()
)

// Route called after a successful login
// Redirect the user to the single-page application "forwarding" the auth token
app.get('/auth/success', (req, res) => 
  const origin = req.cookies[WEB_CLIENT_COOKIE]
  if (origin) 
    // if there is a cookie that contains the URL source, redirect the user to this URL
    // and send the user's token in the URL hash
    const token = req.cookies['feathers-jwt']
    const redirectUrl = `$origin/auth0.html#$token`
    res.redirect(redirectUrl)
   else 
    // otherwise send the static page on the same domain.
    res.sendFile(path.resolve(process.cwd(), 'public', 'success.html'))
  
)

客户端,SPA 中的auth0.html 页面

在 SPA 中,我创建了一个名为 auth0.html 的新 html 页面,它做了 3 件事:

它从哈希中读取令牌 它将其保存在本地存储中(以模仿 Feathers 客户端所做的事情) 它将用户重定向到SPA主页index.html

html代码:

<html>
<body>
  <script>
  function init() 
    const token = getToken()
    if (!token) 
      console.error('No auth token found in the URL hash!')
    
    // Save the token in the local storage
    window.localStorage.setItem('feathers-jwt', token)
    // Redirect to the single-page application
    window.location.href = '/'
  

  // Read the token from the URL hash
  function getToken() 
    const hash = self.location.hash
    const array = /#(.*)/.exec(hash)
    if (!array) return
    return array[1]
  

  init()
  </script>
</body>
</html>

现在在 SPA 中,我可以使用 Feathers 客户端,在应用启动时从本地存储中读取令牌。

如果有道理请告诉我,谢谢!

【讨论】:

这个答案也非常感谢。我认为这更准确地满足了我的需求。我创建了一个稍微不同的解决方案,我很快就会在这里描述它。我也会将此解决方案标记为答案,以防我有时间尝试,或者如果其他人可以报告该解决方案有效。 仅供参考我更新了我的答案,因为我实施的第一个策略(发送带有用户令牌的 cookie)不适用于单独的域。可能需要进行一些调整以使这种方法更安全(例如编码在 URL 哈希中发​​送的令牌?)。【参考方案2】:

如果您还没有这样做,您应该按照这篇文章 (React Login with Auth0) 在您的 React 应用程序上实现身份验证。 如果您已经尝试关注它,请用您遇到的具体问题更新您的问题。

尽管您目前不需要 SSO,但您的应用程序中身份验证的实际实现不会有太大差异。通过使用 Auth0 在您的应用中启用 SSO 主要是启用配置切换。

最后完整参考您的确切场景检查的安全相关方面背后的所有理论:

Auth0 Architecture Scenarios: SPA + API


更新:

我链接的完整场景也涵盖了最全面的场景,其中 API 被大量客户端应用程序访问,这些客户端应用程序甚至可能由不拥有受保护 API 但想要访问其背后数据的第三方开发.

它通过利用目前仅在美国地区可用的最新功能来实现这一点,并且在非常高的水平上可以描述为作为服务交付的 OAuth 2.0 授权服务器。

您的特定场景更简单,API 和客户端应用程序都在同一个实体的控制下,因此您有另一种选择。

选项 1 - 通过 Auth0 利用 API 授权 仅限美国地区(目前)

在这种情况下,您的客户端应用程序在身份验证时将收到一个id_token,该id_token 将用于了解当前已通过身份验证的用户,并且还将收到一个access_token,该access_token 可用于代表经过身份验证的用户。

这明确区分了客户端应用程序和 API; id_token 用于客户端应用程序,access_token 用于 API。

它的好处是授权与身份验证明显分开,您可以通过控制访问令牌中包含的范围对授权决策进行非常细粒度的控制。

选项 2 - 以相同方式在客户端应用程序和 API 中进行身份验证

您可以分别部署您的客户端应用程序和 API,但仍然从概念的角度将它们视为同一个应用程序(您将在 Auth0 中配置一个客户端,同时代表客户端和 API)。

这样做的好处是您可以使用在身份验证完成后获得的id_token 来了解客户端上的用户是谁,并且还可以作为对每个 API 请求进行身份验证的机制。

您必须配置羽毛 API 以验证 Auth0 id_token 作为访问 API 的接受令牌。这意味着您不会使用任何基于 API 身份验证的功能,也就是说,您只需接受 Auth0 向您的应用程序发出的令牌作为验证访问权限的方式。

【讨论】:

很好的答案。谢谢!我实际上复制了您提到的 React Login With Auth0 文章中链接的启动项目,但这还不够。我已经扩展了我的问题,所以它更准确地描述了我面临的问题。如果您能给我进一步的提示,我将不胜感激。 我还在这里看到auth0.com/docs/api-auth/grant/implicit,所涉及的解决方案似乎并未在全球范围内推出。那么也许我尝试做的事情根本不受支持? 由 Auth0 完全提供身份验证和 API 授权的完整方案目前仅在美国地区可用。如果您的帐户在另一个区域,您仍然可以利用身份验证部分;我会用更多细节更新答案。 更好的答案。我添加了一些关于我现在如何进行服务器端身份验证的细节。这是我正在使用的内置羽毛身份验证模块。如果您能进一步指导我如何定制它,我将不胜感激。例如,你认为我可以调整feathers-authentication 模块吗?还是我应该在较低级别使用护照模块? 我对feathersjs的了解为零,所以恐怕帮不上什么忙。如果我不得不表达任何意见,那就是尽可能使用护照,主要是因为您可能会在网上遇到更多示例。

以上是关于在与 api 不同的域上对单页应用进行 Auth0 身份验证的主要内容,如果未能解决你的问题,请参考以下文章

前端/后端分离:Safari 不存储来自 API 的 cookie,该 API 托管在与其前端 SPA 客户端不同的域上

带有 Auth0 路由错误 404 的 Angular 2

从现有的 id_token 获取 Auth0 access_token

ASP MVC 5 在不同域上运行 HTML5 客户端和 Web API 服务器

将静态内容与 Web 服务托管在不同的域上,如何避免跨域?

为啥 Firefox 中的网络字体不能在不同的域上工作?