Your login API should return JWT token and how long it should be live.

Your login API response would be like :

  jwt: your jwt token, 
  duration: in seconds 

Use universal-cookies NPM to store this result into cookies.

For more details how to manipulate with cookies, visit

For setting cookies your code like:

const cookies = new Cookies();
cookies.set(name of cookies, jwt value from API call, {
    maxAge: duration,

Above code store the jwt cookies in browser and after maxAge automatically remove it from browser.

So for identifying session is present or not, you should check after specific interval cookies has present in browser or not. If cookies has present in browser then session is on, otherwise session has expired.


The server side API is setting the HTTPOnly cookie which you wont be able to read in JS. What you need to do it in your react App handle a 401 status error and based on that set a flag isAuthenticated or something as false. Otherwise keep it to be true. With each request to the server HTTPOnly cookie would be sent automatically so you don't need to handle the token inside a cookie. The backend code needs to send a 401 once the cookie is expired, or the logout is requested or the JWT inside a cookie expires.


Before I say anything, you have included app.use(cookieParser()) in index.js right? Because if not, you're gonna need that once you've installed it with npm i cookie-parser

But anyway, a few things:

  1. You can create a PrivateRoute in React, as far as I'm aware this tends to work well to protect routes from unauthorized users.

  2. You can simply store an isAuthenticated in either the state or localStorage: however this will require that you make absolutely sure that a user shouldn't be able to just change the value in the state or add isAuthenticated in localStorage and just spoof authenticity (this is the part that took me the longest to get right).

Anyway, the way I've handled this is that when a user logs in an access token (and a refresh token if it doesn't already exists) are generated from the server and sent to the client in an httpOnly cookie, while this makes it so that you can't do anything with it on the client side with JavaScript as Pavan pointed out in his answer (which is generally a good thing), you should use res.status for validation when you make the fetch request. Here's what my fetch request kind of looks like:

const login = async (user) => {
        const body = JSON.stringify(user);
        return fetch(loginURL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            credentials: 'include', //this is important
            body: body
        }).then(function(res) {
            if (res.status === 200) {
                const id =;
                localStorage.sid = id; //this is temporary
                return res.json()
            } else {
                return res.json()
         // you can ignore this part
        .then(async resp => {
            return resp

Side-note: Your browser automatically handles the httpOnly cookies you send from your server however the credentials: 'include' needs to be included in your subsequent fetch requests.

In one of my main parent components, the login function is called:

login = async (user) => {
  this.setState({ error: null });
  await adapter.login(user).then(data => {
    if (!data.error) {
      this.setState({session: "active"})
    } else if (data.error && data.error.status === 401) {
        // this is so I can handle displaying invalid credentials errors and stuff
        this.setState({ error: data.error, loading: false });

I also have a middleware on the server-side that is run before any of the code in the routes to verify that the user making the request is actually authorized. This is also what handles access token expiration for me; if the access token has expired, I use the refresh token to generate a new access token and send it back in the httpOnly cookie, again with a status of 200 so the user experience isn't jarring. This does of course mean that your refresh token would have to live longer than your access token (I haven't decided how long in my case yet, but I'm thinking either 7 or 14 days) though as far as I'm aware that is okay to do.

One last thing, the parent component I referred to earlier calls a validate function which is a fetch request to the server within its componentDidMount so that the user is verified each time the component mounts, and then I've included some conditional rendering:

    render() {
        return (
          <div className="container">
            !localStorage.sid && <LoginForms {...yourPropsHere}/>
            this.state.loading && <Loading />
            localStorage.sid && this.state.session === "active" && <Route path="/" render={(props) => <Root error={this.state.error} {...props}/>}/>

I've gone the conditional rendering route as I couldn't get PrivateRoute to work properly in the time that I had, but either should be fine.

Hopefully this helps.

Related Query

More Query from same tag