使用oidc-client.js和Identityserver4在React前端进行身份验证

20

最近我正在尝试使用IdentityServer4与React客户端设置身份验证。 我部分地遵循了IdentityServer文档的Adding a JavaScript client教程,链接在这里:https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf,同时使用了Quickstart7_JavaScriptClient文件。

不幸的是,我正在使用React作为我的前端,而我的React知识不足以使用React实现教程中使用的相同功能。

尽管如此,我开始阅读并尝试着入门。我的IdentityServer项目和API已经设置好,并且似乎正常工作(也测试过其他客户端)。

我首先将oidc-client.js添加到我的Visual Code项目中。 接下来,我创建了一个页面,在启动时呈现此页面(命名为Authentication.js),这是包含登录、调用API和注销按钮的地方。 此页面(Authentication.js)如下所示:

import React, { Component } from 'react';
import {login, logout, api, log} from '../../testoidc'
import {Route, Link} from 'react-router';

export default class Authentication extends Component {
    constructor(props) {
      super(props);
    }

    render() {
      return (
        <div>
            <div>  
                <button id="login" onClick={() => {login()}}>Login</button>
                <button id="api" onClick={() => {api()}}>Call API</button>
                <button id="logout" onClick={() => {logout()}}>Logout</button>

                <pre id="results"></pre>

            </div>  

            <div>
                <Route exact path="/callback" render={() => {window.location.href="callback.html"}} />

                {/* {<Route path='/callback' component={callback}>callback</Route>} */}
            </div>
        </div>
      );
    }
  }
在上面被导入的testoidc.js文件中,我添加了所有使用的oidc函数(例如在示例项目中的app.js)。路由部分应该使callback.html可用,我将该文件保留为原样(可能是错误的)。
testoidc.js文件包含以下函数:
import Oidc from 'oidc-client'


export function log() {
  document.getElementById('results').innerText = '';

  Array.prototype.forEach.call(arguments, function (msg) {
      if (msg instanceof Error) {
          msg = "Error: " + msg.message;
      }
      else if (typeof msg !== 'string') {
          msg = JSON.stringify(msg, null, 2);
      }
      document.getElementById('results').innerHTML += msg + '\r\n';
  });
}

var config = {
  authority: "http://localhost:5000",
  client_id: "js",
  redirect_uri: "http://localhost:3000/callback.html",
  response_type: "id_token token",
  scope:"openid profile api1",
  post_logout_redirect_uri : "http://localhost:3000/index.html",
};
var mgr = new Oidc.UserManager(config);

mgr.getUser().then(function (user) {
  if (user) {
      log("User logged in", user.profile);
  }
  else {
      log("User not logged in");
  }
});

export function login() {
  mgr.signinRedirect();
}

export function api() {
  mgr.getUser().then(function (user) {
      var url = "http://localhost:5001/identity";

      var xhr = new XMLHttpRequest();
      xhr.open("GET", url);
      xhr.onload = function () {
          log(xhr.status, JSON.parse(xhr.responseText));
      }
      xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
      xhr.send();
  });
}

export function logout() {
  mgr.signoutRedirect();
} 

有多个问题。当我点击登录按钮时,我被重定向到identityServer的登录页面(这是好的)。当我使用有效凭据登录后,会重定向到我的React应用程序:http://localhost:3000/callback.html#id_token=Token

身份验证项目中的此客户端定义如下:

new Client
                {
                    ClientId = "js",
                    ClientName = "JavaScript Client",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    AllowAccessTokensViaBrowser = true,

                    // where to redirect to after login
                    RedirectUris = { "http://localhost:3000/callback.html" },

                    // where to redirect to after logout
                    PostLogoutRedirectUris = { "http://localhost:3000/index.html" },

                    AllowedCorsOrigins = { "http://localhost:3000" },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "api1"
                    }

                }

虽然看起来回调函数从未被调用,但它仍停留在具有非常长令牌的回调 URL 上。

登录后,getUser 函数始终显示“未登录的用户”,而Call API按钮则表示没有令牌。显然,事情并没有正常工作。我只是不知道哪些地方出了问题。 检查时,我可以看到在本地存储中生成了一个令牌:

enter image description here

此外,当我点击注销按钮时,我被重定向到身份主机的注销页面,但是当我在那里点击注销时,我没有被重定向到客户端。

我的问题是:

  • 我在使用 oidc-client 与 IdentityServer4 结合时走上了正确的轨道吗?
  • 我是否使用了正确的库,或者 react 是否需要与 oidc-client.js 不同的库。
  • 是否有任何教程介绍了使用 react 前端与 IdentityServer4 和 oidc-client(不使用 redux)的组合,我找不到任何教程。
  • 如何/在哪里添加 callback.html,是否应该进行改写?

能否有人指点我正确的方向,这里可能有更多的事情出了问题,但是此时我卡在了甚至不知道从哪里开始。


2
嗨,Nicolas,如果您能够将最小的可再现代码放在某个地方,那么对于其他人来调试和找出问题会更容易。我已经成功将ID4与React集成了。您不需要单独的“callback.html”,在单页面应用程序中可以使用浏览器路由轻松处理所有内容。但是访问您的代码将有助于更快地找到问题所在。 - hazardous
1
@hazardous 感谢您的回复,很抱歉我反应有些晚。由于度假等原因,情况有些忙乱。自从我发布这个问题以来,我的React和ID4知识已经得到了提高,接下来几周我将尝试再次实现它。如果失败了,我会尽快上传一个可重现的版本。如果成功了,我也会在这里发布结果。 - Nicolas
你们这个项目的后端是什么?我问这个问题是因为通常情况下没有必要在前端完全实现OIDC流程,而且在React之外实现整个流程非常简单。例如,我已经完成了许多React项目,其中我的整个OIDC功能都由.NET OIDC中间件处理,这通常是更好的方法。 - Daniel Tabuenca
@DanielTabuenca 如果你的客户端JS应用程序是React或Angular,且全部在浏览器上运行,则无法在React之外实现整个流程。 - Sangeet Agarwal
1个回答

9

IdentityServer4是OIDC的后端实现,因此,您需要使用给定的API在客户端中实现流程。我不知道oidc-client.js文件是什么,但它很可能正在执行您自己可以实现的相同操作。流程本身非常简单:

  1. React应用程序准备请求并重定向用户到带有client_idredirect_uri(以及state、nonce)的Auth服务器
  2. IdentityServer检查client_idredirect_uri是否匹配。
    • 如果用户未登录,请显示登录框
    • 如果需要同意书(类似于通过Facebook / Google登录某些应用程序时),则显示必要的交互
    • 如果用户经过身份验证和授权,则将页面重定向到redirect_uri并附加新参数。在您的情况下,URL将如下所示:https://example.com/cb#access_token=...&id_token=...&stuff-like-nonce-and-state
  3. 现在,React应用程序需要解析URL、访问值并将令牌存储在某个地方以供将来使用:

实现该逻辑最简单的方法是首先在路由器中设置一个路线,解决成一个将执行该逻辑的组件。该组件可以是“不可见”的。它甚至不需要呈现任何内容。您可以像这样设置路由:

<Route path="/cb" component={AuthorizeCallback} />

然后,在AuthorizeCallback组件中实现OIDC客户端逻辑。在该组件中,您只需要解析URL即可。您可以使用location.hash来访问URL的#access_token=...&id_token=...&stuff-like-nonce-and-state部分。您可以使用URLSearchParams或第三方库(如qs)来进行处理。然后,将值存储在某个地方(sessionStorage、localStorage,如果可能的话,还可以使用cookies)。您可以执行的其他任何操作都是具体实现细节。例如,在我的某个应用程序中,为了记住用户在应用程序中所处的活动页面,我将值存储在sessionStorage中,然后在AuthorizeCallback中使用该存储中的值将用户重定向到正确的页面。因此,Auth服务器重定向到"/cb",解析为AuthorizeCallback,而此组件则根据用户所在的位置重定向到所需位置(如果未设置位置,则重定向到"/")。

另外,请记住,如果授权服务器的会话cookie尚未过期,则在令牌过期或已删除时,您将不需要重新登录。这对于令牌过期很有用,但在您登出时可能会出现问题。因此,当您退出时,需要向授权服务器发送请求,以立即删除/过期令牌,然后再从存储中删除该令牌。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接