如何在React单页应用中通过浏览器持久化Auth0登录状态

8

目前在创建路由时,我会检查一个名为 isAuthenticated() 的 Auth0 方法来确定是否返回受保护的页面或重定向到登录页面。然而,这个状态只存在于内存中,不能在浏览器刷新后让用户停留在他们的页面上,我想实现这一点。

这是一个 React/RR4/React Context 应用程序,我的 Auth0 方法列在 Auth.js 中(如下所示)。

将登录状态存储在 localStorage 中是极不建议的。如果我将我的 Auth0 令牌存储在 cookie 中,我不确定如何验证令牌,因为没有设置服务器验证。什么是正确的条件来检查,以实现安全的数据持久性?

ProtectedRoutes.jsx:

   <Route
      exact
      key={route.path}
      path={route.path}
      render={() => (
        // CONDITION TO CHECK
        context.auth.isAuthenticated()
          ? (
            <div>
              <route.component />
            </div>
          ) : <Redirect to="/login" />
        )}
      />

Auth.js(参考内容):

import auth0 from 'auth0-js';
import authConfig from './auth0-variables';

class Auth {
  accessToken;
  idToken;
  expiresAt;
  tokenRenewalTimeout;

  auth0 = new auth0.WebAuth({
    domain: authConfig.domain,
    clientID: authConfig.clientId,
    redirectUri: authConfig.callbackUrl,
    responseType: 'token id_token',
    scope: 'openid'
  });

  constructor() {
    this.scheduleRenewal();
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
    this.handleAuthentication = this.handleAuthentication.bind(this);
    this.isAuthenticated = this.isAuthenticated.bind(this);
    this.getAccessToken = this.getAccessToken.bind(this);
    this.getIdToken = this.getIdToken.bind(this);
    this.renewSession = this.renewSession.bind(this);
    this.scheduleRenewal = this.scheduleRenewal.bind(this);
  }

  login() {
    console.log('logging in!');
    this.auth0.authorize();
  }

  handleAuthentication() {
    return new Promise((resolve, reject) => {
      this.auth0.parseHash((err, authResult) => {
        if (err) return reject(err);
        console.log(authResult);
        if (!authResult || !authResult.idToken) {
          return reject(err);
        }
        this.setSession(authResult);
        resolve();
      });
    });
  }

  getAccessToken() {
    return this.accessToken;
  }

  getIdToken() {
    return this.idToken;
  }

  getExpiration() {
    return new Date(this.expiresAt);
  }

  isAuthenticated() {
    let expiresAt = this.expiresAt;
    return new Date().getTime() < expiresAt;
  }

  setSession(authResult) {
    localStorage.setItem('isLoggedIn', 'true');
    let expiresAt = (authResult.expiresIn * 1000) + new Date().getTime();
    this.accessToken = authResult.accessToken;
    this.idToken = authResult.idToken;
    this.expiresAt = expiresAt;
    this.scheduleRenewal();
  }

  renewSession() {
    this.auth0.checkSession({}, (err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        this.setSession(authResult);
      } else if (err) {
        this.logout();
        console.log(`Could not get a new token. (${err.error}: ${err.error_description})`);
      }
    });
  }

  scheduleRenewal() {
    let expiresAt = this.expiresAt;
    const timeout = expiresAt - Date.now();
    if (timeout > 0) {
      this.tokenRenewalTimeout = setTimeout(() => {
        this.renewSession();
      }, timeout);
    }
  }

  logout() {
    this.accessToken = null;
    this.idToken = null;
    this.expiresAt = 0;
    localStorage.removeItem('isLoggedIn');
    clearTimeout(this.tokenRenewalTimeout);
    console.log('logged out!');
  }
}

export default Auth;
2个回答

8

当浏览器刷新时,您可以使用静默身份验证来更新令牌。

特别适用于您的React SPA应用程序:

  • 在主App组件中设置一个名为tokenRenewed的状态,将其值设为false
  • 您已经在auth.js中编写了一个renewToken方法,因此在componentDidMount方法中调用该方法。
componentDidMount() {
   this.auth.renewToken(() => {
      this.setState({tokenRenewed : true});
   })
}
  • renewToken更新为接受以下形式回调函数cb
renewSession(cb) {
    this.auth0.checkSession({}, (err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        this.setSession(authResult);
      } else if (err) {
        this.logout();
        console.log(`Could not get a new token. (${err.error}: ${err.error_description})`);
      }
      if(cb) cb(err, authResult);
    });
  }
  • 确保只有在通过静默身份验证更新了有效令牌后,您才加载 App 组件,即 tokenRenewedtrue
render() {
    if(!this.state.tokenRenewed) return "loading...";
    return (
      // Your App component
    );
}

注释:

  1. 为确保此功能正常工作,您可能需要在应用程序设置中设置正确的允许的Web来源
  2. 静默身份验证存在一些限制,因为它需要浏览器启用第三方Cookie,并且在Safari中需要启用ITP。您应该设置自定义域来避免这种情况。请参考官方的auth0文档了解更多信息here
  3. 有关如何安全地存储令牌的更多详细信息,请单击here

2
我想要补充的是,如果路由已经重定向,状态改变可能不起作用,因此我将重定向组件更改为一个带动画的加载器。 - evaline

3

我想在这里引用官方的Auth0文档。

默认情况下,Auth0 SPA SDK将令牌存储在内存中。但是,这并不提供跨页面刷新和浏览器选项卡的持久性。相反,您可以选择通过在初始化SDK时将cacheLocation属性设置为localstorage来将令牌存储在本地存储中。这可以帮助缓解一些防止访问Auth0会话cookie的浏览器隐私技术的影响,通过更长时间地存储访问令牌。

将令牌存储在浏览器本地存储中可提供跨页面刷新和浏览器选项卡的持久性。但是,如果攻击者可以使用跨站脚本(XSS)攻击在SPA中运行JavaScript,则可以检索存储在本地存储中的令牌。导致成功的XSS攻击的漏洞可能在SPA源代码中或包括在SPA中的任何第三方JavaScript代码(例如bootstrap、jQuery或Google Analytics)中。

来源

简而言之,文档从未说过将令牌存储在本地存储中本身就是安全问题。XSS漏洞(与应用程序中的auth0实现无关),因为窃取用户令牌只是攻击者可以做的一系列坏事中的一件。

我喜欢接受的答案,因为它提供了一个解决我在auth0中遇到的问题的工作解决方案。但我认为刷新不应该在每次页面重新加载时进行,而应该在过期时进行。

此外,我在不同的浏览器中有不同的行为。这个注销问题偶尔会在Chrome中发生,在Safari中每隔一秒钟发生一次。我认为实现必须可靠,并且对于所有流行的浏览器/版本都能够一致地工作。

如何应用设置的示例:

<Auth0Provider
  domain={domain}
  clientId={clientId}
  redirectUri={redirectUri}
  onRedirectCallback={onRedirectCallback}
  audience={audience}
  // Possible values: memory, localstorage, sessionstorage or cookies
  cacheLocation={'localstorage'}
>
  {children}
</Auth0Provider>

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