56

New to react and working on an application with authentication/logging in. It currently works but feels hacked together. Right now I have my isAuthenticated state located in my routes.js like so:

class Routes extends Component {

    constructor(props) {
        super(props);

        this.state = {
            isAuthenticated: false,
         }
     }

On my login page, I need to know when a user is authenticated to redirect them to the home page. What is the best design pattern to allow access and manipulation of this isAuthenticated state? How I currently have it set up is I have a function that sets the state inside the routes.js and sends the state as a prop like so:

 setAuthenticated = (isAuthenticated) => {
        this.setState({isAuthenticated});
    }

and down below in the router...

<Route path="/" exact component={() =>
                            <div>
                                <Login
                                    isAuthenticated={this.state.isAuthenticated}
                                    setAuthenticated={this.setAuthenticated}
                            </div>
                        } />

Yes, I understand this is bad design because this is changing props values which are supposed to be immutable. This is also bad because when I change this value in my login.js it causes multiple unnecessary re-renders. Should I be declaring isAuthenticated as some type of global variable? I am not using any state management by the way.

Edit: I am setting isAuthenticated based on a response from my server which confirms correct login/password combination.

GG.
  • 21,083
  • 14
  • 84
  • 130
Vincent Nguyen
  • 1,555
  • 4
  • 18
  • 33
  • If your page protection relies on an 'isAuthenticated' state variable, you should probably [disable the react devtools](https://stackoverflow.com/questions/33783620/disable-chrome-react-devtools-for-production) in production. Otherwise, it would be possible to inspect the page and manually flip the flag to true, exposing the protected page to an unauthenticated user. – bje Jun 24 '19 at 23:31
  • @bje No security stuff should be implemented on the front-end side. Things like redirecting user to home page and ... are just for better UX not strict security rules – A.Mohammadi Jun 24 '22 at 18:14

4 Answers4

139

Handling isAuthenticated only in the state means the user will be unauthenticated every time he refreshes the page. That's not really user-friendly! :)

So instead, the Login page should store an access_token (coming from your backend) in the cookies or localStorage of the browser. An access_token proves the user is authenticated and also verifies his identity. You will usually pass this access_token to every next requests to your server, to check if this user is allowed to access the data he's requesting, or allowed to create, edit and delete the things he's trying to create, edit and delete.

Then you can check this access_token on every other pages as well and redirect the user to the Login page if he's not authenticated anymore.


A brief aside on the difference between access_token and refresh_tokenthis will help you understand the code bellow, but feel free to skip ahead if you are already familiar with it.

Your backend probably uses OAuth2, which is the most common authentication protocol nowadays. With OAuth2, your app makes a first request to the server containing the username and password of the user to authenticate. Once the user is authenticated, he receives 1) an access_token, which usually expires after an hour, and 2) a refresh_token, which expires after a very long time (hours, days). When the access_token expires, instead of asking the user for his username and password again, your app sends the refresh_token to the server to obtain a new access_token for this user.


A brief aside on the differences between cookies and localStoragefeel free to skip it too!

localStorage is the most recent technology between both. It's a simple key/value persistence system, which seems perfect to store the access_token and its value. But we also need to persist its date of expiration. We could store a second key/value pair named expires but it would be more logic to handle on our side.

On the other hand, cookies have a native expires property, which is exactly what we need! cookies are an old technology and are not very developer-friendly, so I personally use js-cookie, which is a small library to manipulate cookies. It makes it look like a simple key/value persistence system too: Cookies.set('access_token', value) then Cookies.get('access_token').

Other pro for the cookies: they are cross subdomains! If your Login app is login.mycompany.com and your Main app is app.mycompany.com, then you can create a cookie on the Login app and access it from the Main app. This is not possible with LocalStorage.


Here are some of the methods and special React components I use for authentication:

isAuthenticated()

import Cookies from 'js-cookie'

export const getAccessToken = () => Cookies.get('access_token')
export const getRefreshToken = () => Cookies.get('refresh_token')
export const isAuthenticated = () => !!getAccessToken()

authenticate()

export const authenticate = async () => {
  if (getRefreshToken()) {
    try {
      const tokens = await refreshTokens() // call an API, returns tokens

      const expires = (tokens.expires_in || 60 * 60) * 1000
      const inOneHour = new Date(new Date().getTime() + expires)

      // you will have the exact same setters in your Login page/app too
      Cookies.set('access_token', tokens.access_token, { expires: inOneHour })
      Cookies.set('refresh_token', tokens.refresh_token)

      return true
    } catch (error) {
      redirectToLogin()
      return false
    }
  }

  redirectToLogin()
  return false
}

redirectToLogin()

const redirectToLogin = () => {
  window.location.replace(
    `${getConfig().LOGIN_URL}?next=${window.location.href}`
  )
  // or history.push('/login') if your Login page is inside the same app
}

AuthenticatedRoute

export const AuthenticatedRoute = ({
  component: Component,
  exact,
  path,
}) => (
  <Route
    exact={exact}
    path={path}
    render={props =>
      isAuthenticated() ? (
        <Component {...props} />
      ) : (
        <AuthenticateBeforeRender render={() => <Component {...props} />} />
      )
    }
  />
)

AuthenticateBeforeRender

class AuthenticateBeforeRender extends Component {
  state = {
    isAuthenticated: false,
  }

  componentDidMount() {
    authenticate().then(isAuthenticated => {
      this.setState({ isAuthenticated })
    })
  }

  render() {
    return this.state.isAuthenticated ? this.props.render() : null
  }
}
GG.
  • 21,083
  • 14
  • 84
  • 130
  • Thanks for the well rounded answer. I am setting `isAuthenticated` after authenticated on my server and sending an ok response. So after I get a good authentication response I should store it in localStorage like the first answer suggests, correct? – Vincent Nguyen Apr 13 '18 at 14:43
  • You probably need a token from your server. When your app will call your server APIs to retrieve some data, you want to pass a header `Authorization: token` in each request to prove the identity of the author of the request. – GG. Apr 13 '18 at 14:50
  • 5
    Since you're accessing the cookies from the code, it means they are not HttpOnly, does that mean a CSS vulnerability is present? – manash Aug 28 '19 at 17:50
  • 3
    @MickaelMarrache Yes, this method is vulnerable to XSS attacks. Auth cookies should be http only, this answer is bad bad bad. – n0rd Apr 29 '20 at 11:32
  • @MickaelMarrache No, there are no XSS attack vectors in this answer and the examples I posted. – If your app is vulnerable to XSS attacks, indeed an attacker could steal cookies as well as anything else (passwords, emails, phone and credit card numbers, etc.). You should probably audit and review your code if you think your app is vulnerable to these attacks. Here are some resources about how to prevent them: https://owasp.org/www-community/attacks/xss. I also recommend you to check this answer about some XSS attack vectors in React (with examples): https://stackoverflow.com/a/51852579/652669 – GG. Apr 29 '20 at 19:12
  • 10
    This is indeed a dangerous and insecure way to store authentication tokens. Cookies can be set with an `HTTPOnly` flag which exists specifically to limit the damage an XSS attack can cause. Your response is hyperbolic in the extreme: An XSS vulnerability in a webapp does *not* let it steal cookies, *if they are properrly secured*, which they are not in your example, and claiming XSS can steal "passwords, emails, phone and credit card numbers" is absurd. The fact is this answer is dangerously insecure; your front-end JS should not be handling auth tokens. – user229044 Apr 30 '20 at 02:36
  • Note that your own links to OWASP recommend using HTTPOnly cookies, which is impossible if your setting the cookie via JS, which you are doing: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#bonus-rule-1-use-httponly-cookie-flag – user229044 Apr 30 '20 at 02:37
  • Also note: It is extremely incorrect and dangerous to expose the `refresh_token` to the browser, which you also appear to be doing. This is a *massive* no-no in OAuth2, and it shouldn't even be possible, since it seems like you're using the implicit flow, which usually cannot issue refresh tokens for this very reason. – user229044 Apr 30 '20 at 02:42
  • @meagar I feel like you took this answer out of context (and I know you are here only because I flagged a comment). The question is about React single page applications (SPAs). My answer is for a scenario where all the requests between the SPA and the server go through a REST API. Now each request requires an `Authorization` header containing an access token, which I need to store somewhere. The most secure thing to do would be to store the token only in memory (a JS variable in the app). Problem is, the users will have to log in again every time they refresh the page or open a new tab. – GG. Apr 30 '20 at 07:35
  • @meagar This is a _massive_ no-no for most users, so I need to store the token somewhere else to persist the "session". Even if I could store the token in a `HTTPOnly` cookie, I won't be able to read its value (JavaScript can't access it). So my only options are localStorage, JS cookies (which I both compare in my answer) and other browser storage technologies. I think (and lots of people, since it's a pretty common pattern with SPAs to store tokens in browser storage) this trade off between security and user-experience is OK with proper XSS preventions and token expiration on the server. – GG. Apr 30 '20 at 07:37
  • @meagar Here are some links about this topic: https://security.stackexchange.com/a/175785/26797, https://security.stackexchange.com/a/179507/26797, https://security.stackexchange.com/a/220059/26797, https://www.reddit.com/r/webdev/comments/bpcleu/so_whats_the_issue_with_jwts_in_localstorage, https://auth0.com/docs/tokens/concepts/token-storage#browser-in-memory-scenarios, https://github.com/auth0/angular2-jwt/blob/master/README.md#tokengetter-function. Also, I'm going to check what you said about the `refresh_token` and update my answer if you are right! – GG. Apr 30 '20 at 07:47
  • @GG., thanks for the really helpful and thorough answer! Could you please recommend a JS library / package that implements an authentication strategy like the one proposed in your answer? – ezpresso Apr 29 '21 at 22:41
  • @ezpresso For React? Surprisingly, I don't think there are any reputable libraries with components and/or hooks for authentication. Seems like everybody is (re)implementing its own homemade solution. – GG. Apr 29 '21 at 23:12
  • @GG. Oh, thanks! That's really surprising. What about any general purpose authentication libraries for JS / Node? Anything other than Passport.js. – ezpresso Apr 29 '21 at 23:37
  • What's the consensus here on this approach's security in 2022? – LinusGeffarth Sep 06 '22 at 21:47
6

If you are using an application where the authentication lasts only for one session, storing it in state is enough. But do note that this means, the user will lose the authenticated status on page refresh.

Here is an example using React Context, where we create context using createContext and use Consumer to access it across the application.

const AuthenticationContext = React.createContext();
const { Provider, Consumer } = AuthenticationContext;

function Login(props) {
  return (
    <Consumer>
      {
        value=>
        <button onClick={value.login}>Login</button>
      }
    </Consumer>
  );
}

function Logout() {
  return (
    <Consumer>
      {
        value=>
        <button onClick={value.logout}>Logout</button>
      }
    </Consumer>
  );
}

function AnotherComponent() {
  return (
    <Consumer>
      {
        value=>{
          return value.isAuthenticated?
            <p>Logged in</p>:
            <p>Not Logged in</p>
        }
      }
    </Consumer>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.login = ()=> {
      this.setState({
        isAuthenticated: true
      });
    }
    this.logout = ()=> {
      this.setState({
        isAuthenticated: false
      });
    }
    this.state = {
      isAuthenticated: false,
      login: this.login,
      logout: this.logout
    }
  }
  
  render() {
    return (
      <Provider value={this.state}>
        <Login />
        <Logout />
        <AnotherComponent />
      </Provider>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div> 

https://reactjs.org/docs/context.html#reactcreatecontext

Agney
  • 18,522
  • 7
  • 57
  • 75
4

you can set the access token in the local storage on login and clear it after the user logs out. the is authenticated method will then be used to check if there is a token and whether the token is valid while making an API call

Waweru Mwaura
  • 247
  • 1
  • 7
-3

One of the best ways to use the React-Auth-Kit library. It has all the features, that can be used with react.

Link: https://www.npmjs.com/package/react-auth-kit