The React Mega-Tutorial, Chapter 8: Authentication

Posted by
on under

Up to this point, you have been using the Microblog API back end with an option to bypass authentication. This enabled the project to grow without having to deal with the highly complex matter of user authentication up front. In this chapter you will finally learn how to do this.

The complete course, including videos for every chapter is available to order from my courses site. Ebook and paperback versions of this course are also available from Amazon. Thank you for your support!

For your reference, here is the complete list of articles in this series:

Enabling Back End Authentication

Back in Chapter 5 you installed the Microblog API back end, and as part of the setup instructions, you added the DISABLE_AUTH=true configuration option.

You now need to change this option to DISABLE_AUTH=false. If you deployed Microblog API locally, either in a Docker container or as a Python process, change this setting in the .env file located in the top-level directory of the Microblog API project, and then stop and restart your back end for the change to take effect.

If you deployed Microblog API on Heroku, visit your application dashboard and select your deployed application to reconfigure it. From the application page select "Settings", scroll down to "Config vars" and click the "Reveal Config Vars" button to edit the value of DISABLE_AUTH. Heroku automatically restarts your application after the configuration is changed.

Keep in mind that as soon as you activate authentication on the back end, your React application will lose the ability to communicate with it, as many requests will now return a 401 status code and deny access. For requests that render a spinner while data is loaded, the spinner will stay visible indefinitely. All these issues are going to be fixed as the authentication is implemented in this chapter.

Authentication in the API Client

The most important benefit of having a dedicated API client class is that all aspects of working with the API are implemented in a single place. Because authentication support will be added inside the class, none of the application components that make API calls will need to change.

The Microblog API authentication flow is as follows:

  • The client sends a POST request to /api/tokens, passing the username and password in a standard basic authentication header.
  • If the credentials are correct, the status code in the response is 200, and the body includes the access_token attribute. To enable token security best practices, the access token has a short life, and a refresh token that can be used to renew it is returned in a secure and HTTP-only cookie.
  • To make authenticated API calls, the client must include a standard Bearer authentication header with a valid access token in all requests.
  • If an API call is made with an access token that is incorrect or expired, a response with status code 401 is returned.

While in general terms the solution presented in this chapter can be adapted to fit your own projects, you will need to customize the specific authentication flow according to the requirements of your server.

Passing the Access Token in API Calls

The fetch() call in the request() method needs to be expanded to include an Authorization header with the Bearer scheme.

src/MicroblogApiClient.js: Include bearer token header

      response = await fetch(this.base_url + options.url + query, {
        method: options.method,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ' + localStorage.getItem('accessToken'),
          ...options.headers,
        },
        body: options.body ? JSON.stringify(options.body) : null,
      });
}

The access token is retrieved from the browser's local storage. You will see later that when an access token is returned by the back end, it is immediately written to this storage. If there is no previously stored access token in the local storage, then null will be returned.

You may be wondering why this code isn't checking that the access token is not null before including it in the request. The assumption is that if the client object doesn't have an access token, then a different Authorization header will be included in the options object passed by the caller, which overrides the Authorization header defined here.

Storing the token in local storage makes it possible for the application to "remember" the authenticated user when the user refreshes the browser page, when the site is opened in multiple tabs, and when the browser is closed and reopened, which is a behavior that most users expect from a web application. However, you should keep in mind that storing sensitive information in local storage presents a risk if your application is vulnerable to cross-site scripting (XSS) attacks.

An alternative implementation that avoids the risk of getting tokens compromised in an XSS attack is to use an instance variable in the MicroblogApiClient class to store the access token instead of local storage. While this solution increases security, the user experience would be severely degraded, as users would be required to authenticate every time a session with the application is started, even after a page refresh.

Cross-Site Scripting Attacks

You've learned in the previous section that storing sensitive information in the browser's local storage is sometimes considered a security risk, yet this is exactly what this project is doing. So this is a good time to discuss these risks in detail.

An XSS attack involves the attacker figuring out a way to insert malicious JavaScript into an application running on the user's browser. There are two basic ways in which this can happen:

  • The attacker finds a way to break into the server that hosts the application's JavaScript files and makes modifications to them so that hacked versions with malicious code are served to clients.
  • The attacker tricks the application running in the browser into rendering a <script> tag with malicious JavaScript code.

If you are using a third-party service such as Heroku to serve your React application, then the first scenario is the responsibility of your service provider. If you host your React application yourself, then you must use standard server hardening techniques such as passwordless logins, use of a firewall, closing any unnecessary network ports, etc.

React provides decent protection against the second attack vector, as long as you render all the content in your application through JSX. Protection against XSS attacks in React consists in applying escaping to all the text that is included in JSX contents returned by components. This escaping is always applied, there is no need to enable this protection.

To keep your application well protected, it is extremely important to avoid the temptation to bypass JSX and render contents to the page directly through DOM APIs, as this would not have any protection against XSS attacks.

When all these security concerns are addressed, the risk of a React application being the victim of an XSS attack is extremely low. Even though it would be unlikely for an access token to be compromised, it is considered a good practice to use short expirations on these tokens, so that if an attacker manages to steal a token by some unknown attack method, the damage that can be done with it is limited. Microblog API provides access tokens that last only 15 minutes in the default configuration. The tokens can only be renewed with a refresh token that is stored in a secure cookie, inaccessible from the browser's JavaScript environment.

Logging In and Out

How is the access token obtained? To receive a token, the client must send a POST request to /api/tokens, with the username and password entered by the user on the login page.

Instead of sending this request directly from the LoginPage component, the MicroblogApiClient class can implement a login() method with the required logic that generates the basic authentication header.

src/MicroblogApiClient.js: Login method

  async login(username, password) {
    const response = await this.post('/tokens', null, {
      headers: {
        Authorization:  'Basic ' + btoa(username + ":" + password)
      }
    });
    if (!response.ok) {
      return response.status === 401 ? 'fail' : 'error';
    }
    localStorage.setItem('accessToken', response.body.access_token);
    return 'ok';
  }

Here you can see how the Authorization header is passed as an option in the third argument to api.post(), to override the default bearer token header added in the previous section. The second argument is used for the body of the request, which is set to null because in this particular case there is no body required.

In case you are curious, the Authorization header used in this method follows the Basic Authentication format defined by the HTTP specification, which requires encoding the username and password as a base64 string, done here by the btoa() function available in JavaScript.

There are three possible outcomes for this function. The two most obvious ones indicate authentication success or failure. A third less likely case occurs when the authentication request fails due to an unexpected issue, and not because the credentials are invalid. The return value of this function can be ok, fail or error to represent these three cases. The error case occurs when the authentication request returns a failure status code other than 401.

If the server returns a successful response, then the access token is written to the browser's local storage, so that it is used in all subsequent requests.

The user should be given the option to log out of the application when desired. To log a user out, the access token should be removed. A helper logout() method in the MicroblogApiClient class performs this action.

src/MicroblogApiClient.js: Logout method

  async logout() {
    await this.delete('/tokens');
    localStorage.removeItem('accessToken');
  }

The logout() method makes a request to the token revocation endpoint of Microblog API, which ensures that the access token cannot be used again. It also removes the token from local storage so that the React application completely forgets about it.

For convenience, an isAuthenticated() method is also added to the client class. The application can use it to check if there is an authenticated user or not.

src/MicroblogApiClient.js: isAuthenticated method

  isAuthenticated() {
    return localStorage.getItem('accessToken') !== null;
  }

A User Context and Hook

An important function of the authentication system is to provide information about the logged-in user to any components that need it. The preferred way to share the user with the application's components is with a context and hook combination.

The UserContext will share an object with four attributes:

  • user: the currently logged-in user, or null if the user is not logged in. A value of undefined indicates that the user is being retrieved from the server.
  • setUser: a setter for the user.
  • login: a helper function to log the user in with the username and password provided.
  • logout: a helper function to log the user out.

The complete implementation of the user context and its associated hook function is shown below.

src/contexts/UserProvider.js: User context and hook

import { createContext, useContext, useState, useEffect } from 'react';
import { useApi } from './ApiProvider';

const UserContext = createContext();

export default function UserProvider({ children }) {
  const [user, setUser] = useState();
  const api = useApi();

  useEffect(() => {
    (async () => {
      if (api.isAuthenticated()) {
        const response = await api.get('/me');
        setUser(response.ok ? response.body : null);
      }
      else {
        setUser(null);
      }
    })();
  }, [api]);

  const login = async (username, password) => {
    const result = await api.login(username, password);
    if (result === 'ok') {
      const response = await api.get('/me');
      setUser(response.ok ? response.body : null);
    }
    return result;
  };

  const logout = async () => {
    await api.logout();
    setUser(null);
  };

  return (
    <UserContext.Provider value={{ user, setUser, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

export function useUser() {
  return useContext(UserContext);
}

The UserProvider component defines a user state variable that will contain the details of the authenticated user. The state variable is initialized with a value of undefined, which is used while the user is being retrieved from the server. The value will change to the user details returned by the server as soon as they are available. The value will change to null if there is no authenticated user.

A side effect function is used to try to resolve the value of the user state variable when the component is rendered for the first time. When the api instance is authenticated, the function loads the user's details by calling the /api/me endpoint of Microblog API. The state variable remains set to the initial undefined value until the user information is returned, so any components that need to render user information can display a spinner or similar UI component while waiting.

If the api instance does not have an access token, then the user state variable is set to null to indicate to the rest of the application that there is no user logged in.

The login() helper function accepts username and password arguments. This function will be called by the LoginPage component to log the user in. The function starts by invoking the method of the same name in the API client. If the authentication attempt succeeds, a request to retrieve user information is issued, and the user state variable is updated accordingly, in a very similar way to how it was done in the side effect function.

The logout() helper function logs the user out in the api client instance, and then updates the user state variable to null, so that all components that use the context know that there is no authenticated user anymore.

The UserContext.Provider component sets the value of the context as an object with the user, setUser, login and logout keys.

As with all previous contexts, a useUser() companion hook function is defined to make it more convenient for components to access the elements of the context. For this hook, the entire object shared in the context is returned. The components accessing the context can use a destructuring assignment to obtain the attributes that they need.

The user context needs to be added to the application. Since this context depends on having access to the API client, it needs to be a child of it. Below is the updated App component.

src/App.js: Add user context

... // <-- no changes to existing imports
import UserProvider from './contexts/UserProvider';

export default function App() {
  return (
    <Container fluid className="App">
      <BrowserRouter>
        <FlashProvider>
          <ApiProvider>
            <UserProvider>
              <Header />
              <Routes>
                ... // <-- no changes to routes
              </Routes>
            </UserProvider>
          </ApiProvider>
        </FlashProvider>
      </BrowserRouter>
    </Container>
  );
}

Implementing Private Routes

When designing the authentication subsystem of the application, you have to decide what is the subset of the application routes that are going to be available only to users that have previously authenticated. Let's call these the private routes of the application.

When a client that is not authenticated attempts to access a private route, the best course of action is to redirect the user to the login page, and once the user submits the authentication form, redirect back to the route that was initially attempted.

The PrivateRoute component shown below can be used as a parent for any components that are only allowed to render when there is an authenticated user.

src/components/PrivateRoute.js: Private route component

import { useLocation, Navigate } from 'react-router-dom';
import { useUser } from '../contexts/UserProvider';

export default function PrivateRoute({ children }) {
  const { user } = useUser();
  const location = useLocation();

  if (user === undefined) {
    return null;
  }
  else if (user) {
    return children;
  }
  else {
    const url = location.pathname + location.search + location.hash;
    return <Navigate to="/login" state={{next: url}} />
  }
}

The component's render function uses the useUser() hook from the previous section to retrieve the user state variable. If this variable is undefined, it means that the application isn't ready to provide user information yet. In this situation, this component cannot do anything, so it returns null to not render anything. This isn't a problem because the undefined value for the user is temporary. As soon as the user state variable in the user context resolves to null or to the user's details, this component will re-render.

When the user is set to a truthy value, it means that there is an authenticated user, and in that case this component renders its children.

The interesting case is when user is null, indicating that the user is not authenticated. In this situation, the children of this component cannot be rendered, because they require a user to be logged in. The component renders a Navigate component from React-Router, which sends the user to the login page is rendered instead.

As mentioned earlier, the intention is to redirect the user back to the original page after authentication is completed, so it is necessary to store the current URL so that it can be used later. To determine what is the current URL, React-Router provides a useLocation() hook, which returns a location object, very much like the browser's own document.location. From this object, the pathname, search and hash attributes are concatenated to form a single URL string to save.

The Navigate component supports a state prop in which the application can store any custom data to be preserved in the location object. This is the perfect place to write the URL that the user needs to be sent back to after authentication. The URL is stored in the state prop in an object, under a next key.

Public Routes

The login and user registration routes have the interesting property that they have no purpose after the user logs in. To prevent a logged-in user from navigating to these routes, it is a good idea to also create a PublicRoute component that only allows its children to render when the user is not logged in, or else it redirects the user to the root URL of the application.

Below is the complete implementation of PublicRoute, following a style that is very similar to that of PrivateRoute.

src/components/PublicRoute.js: Public route component

import { Navigate } from 'react-router-dom';
import { useUser } from '../contexts/UserProvider';

export default function PublicRoute({ children }) {
  const { user } = useUser();

  if (user === undefined) {
    return null;
  }
  else if (user) {
    return <Navigate to="/" />
  }
  else {
    return children;
  }
}

Routing Public and Private Pages

How are the PrivateRoute and PublicRoute components defined in the previous sections used? There are a couple of different options, but these components act as wrappers of the route components.

A simple method to use these components is to add them directly in the Route definitions, wrapping the intended page components. For example:

<Route path="/explore" element={
  <PrivateRoute><ExplorePage /></PrivateRoute>
} />

Given that most pages in this application are going to be private, it can get noisy to have lots of PrivateRoute wrappers. A less verbose alternative is to separate the public and private routes and then use a single PrivateRoute wrapper for the entire group of private routes.

Below are the changes to App to apply the route wrappers:

src/App.js: Routing of public and private routes

... // <-- no changes to existing imports
import PrivateRoute from './components/PrivateRoute';
import PublicRoute from './components/PublicRoute';

export default function App() {
  return (
    ... // <-- no changes to outer components

    <Routes>
      <Route path="/login" element={
        <PublicRoute><LoginPage /></PublicRoute>
      } />
      <Route path="/register" element={
        <PublicRoute><RegistrationPage /></PublicRoute>
      } />
      <Route path="*" element={
        <PrivateRoute>
          <Routes>
            <Route path="/" element={<FeedPage />} />
            <Route path="/explore" element={<ExplorePage />} />
            <Route path="/user/:username" element={<UserPage />} />
            <Route path="*" element={<Navigate to="/" />} />
          </Routes>
        </PrivateRoute>
      } />
    </Routes>

    ... // <-- no changes to outer components
  );
}

With this solution, only the public routes of the application are defined as top-level routes. The two public routes that map to the login and registration pages need to be disabled once the user logs in, so both are wrapped individually with PublicRoute.

If none of the public routes are a match, then a catch-all * route is defined with an inner <Routes> component that includes the remaining routes, which are all private and are wrapped with a single PrivateRoute component.

Hooking Up the Login Form

Most of the low-level pieces of the authentication solution are now in place, so the LoginPage component can now be completed, so that it actually performs the authentication procedure.

src/pages/LoginPage.js: Log users in

import { useState, useEffect, useRef } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Body from '../components/Body';
import InputField from '../components/InputField';
import { useUser } from '../contexts/UserProvider';
import { useFlash } from '../contexts/FlashProvider';

export default function LoginPage() {
  ... // <-- no changes to existing state and references
  const { login } = useUser();
  const flash = useFlash();
  const navigate = useNavigate();
  const location = useLocation();

  ... // <-- no changes to side effect function

  const onSubmit = async (ev) => {
    ... // <-- no changes to existing submit logic

    const result = await login(username, password);
    if (result === 'fail') {
      flash('Invalid username or password', 'danger');
    }
    else if (result === 'ok') {
      let next = '/';
      if (location.state && location.state.next) {
        next = location.state.next;
      }
      navigate(next);
    }
  };

  ... // <-- no changes to returned JSX
}

From the four hooks used in this function, only useNavigate() is new. This hook is from React-Router, and provides access to a navigate() function that is similar to the Navigate component, but in function form.

The new logic to log the user in is added at the end of the onSubmit() function, after all the validation checks have passed. Note that this function was converted to async, so that await can be used.

To initiate the authentication, the login() function from the user context is called with the username and password values entered by the user in the form. The login() function is asynchronous, so it is awaited. The return value is the string ok when the user is successfully authenticated, fail when the authentication fails, or error when an unexpected error prevented authentication to be carried out.

In case of an authentication failure, an error message is flashed, and the user remains on the login page to try again.

If the authentication call succeeds, then the API client now has an access token and the user context has the user details to share with other components, so the user can be redirected to any of the private routes of the application.

Normally the redirect is to the root URL, which is the user's feed page, but if location.state has a next attribute, then the redirect goes to this route, which is the one that was saved by the PrivateRoute component when the user attempted to access the page without being logged in.

The third possibility occurs when the authentication call fails due to an unexpected reason. This case is not handled by this component and will instead be handled later with an application-wide error handler.

The application now has a working login procedure. You should be able to register a new user (if you haven't done that already), and then log in. Once you are logged in, your feed page is going to be empty, since you aren't following any users yet. You can navigate to the Explore page to confirm that blog posts from other users are displayed.

As discussed above, access tokens returned by Microblog API are valid for 15 minutes, after which they expire. At this time the MicroblogApiClient class does not have the ability to "refresh" an expired access token, so the application may appear to stop working if you use it long enough for the access token to expire. Refresh support for tokens will be added soon, but in the meantime, if you reach the token expiration time you will need to refresh the page to trigger a new login.

User Information in the Header

Application components can now add logic to render themselves differently when a user is logged in versus when not. This makes it possible to add a common user interface component found in many websites: a dropdown in the top-right corner with account related options.

Below is the new version of Header that includes a dropdown with options to access the user's profile page and to logout of the application.

src/components/Header.js: Show a user account dropdown

import Navbar from 'react-bootstrap/Navbar';
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import NavDropdown from 'react-bootstrap/NavDropdown';
import Image from 'react-bootstrap/Image';
import Spinner from 'react-bootstrap/Spinner';
import { NavLink } from 'react-router-dom';
import { useUser } from '../contexts/UserProvider';

export default function Header() {
  const { user, logout } = useUser();

  return (
    <Navbar bg="light" sticky="top" className="Header">
      <Container>
        <Navbar.Brand>Microblog</Navbar.Brand>
        <Nav>
          {user === undefined ?
            <Spinner animation="border" />
          :
            <>
              {user !== null &&
                <div className="justify-content-end">
                  <NavDropdown title={
                    <Image src={user.avatar_url + '&s=32'} roundedCircle />
                  } align="end">
                    <NavDropdown.Item as={NavLink} to={'/user/' + user.username}>
                      Profile
                    </NavDropdown.Item>
                    <NavDropdown.Divider />
                    <NavDropdown.Item onClick={logout}>
                      Logout
                    </NavDropdown.Item>
                  </NavDropdown>
                </div>
              }
            </>
          }
        </Nav>
      </Container>
    </Navbar>
  );
}

The component gets the currently logged-in user from the useUser() hook. Right after the Navbar.Brand component, a conditional JSX expression is inserted. If the current user is undefined, meaning that the application is still trying to figure out who the user is, a Spinner component is rendered on the right side of the navigation bar.

When the user is not undefined or null, a dropdown is created using Nav components from React-Bootstrap. The case of user being null is handled by not rendering anything in this part of the navigation bar. An alternative that might work for many applications is to render a login link or button, but given that this application automatically redirects to the login form, this does not seem necessary.

The dropdown is created with the avatar image of the logged-in user, which is available in user.avatar_url. As with avatar images displayed in blog posts, the image is provided by the Gravatar service for the email registered in the account. For emails that do not have an avatar image registered with the service, Gravatar returns a unique geometric design. You can associate an image with your email address by visiting the Gravatar site.

The "Profile" option is created using the NavDropdown.Item component from React-Bootstrap, customized to be a NavLink component from React-Router. Recall that this technique was also used to create React-Router compatible links in the sidebar. The to prop of the link is dynamically set to the /api/user/{username} URL for the logged-in user.

The logout option in the dropdown has an onClick handler that invokes the logout() function obtained from the useUser() hook, which logs the user out and then redirects to the main route of the application.

The NavDropdown.Divider component creates a divider line between the two options. In case you want to learn more about creating great looking sidebars, the design of this dropdown is based on the examples of navigation bars with dropdowns in the NavDropdown documentation.

To ensure that the options in this dropdown have a look that is consistent with the links in the sidebar, the foreground and background color definitions added earlier for the sidebar can be extended to also apply to the .dropdown-item.active class.

src/index.css: Styling of dropdown options

... // <-- no changes to other styles

.Sidebar a, .dropdown-item, .dropdown-item.active {
  color: #444;
}

.Sidebar .nav-item .active, .dropdown-item.active {
  background-color: #def;
}

Figure 8.1 shows the account dropdown.

Figure 8.1: User account dropdown

Handling Refresh Tokens

If the client receives a 401 status code while using a previously valid access token, then this indicates that the token has expired and needs to be "refreshed".

As mentioned before, using short-lived tokens is a well accepted security practice in APIs, designed to limit the potential damage that can be caused if an access token is compromised.

When the access token expires, the client can request a new access token by sending a PUT request to /api/tokens, passing the expired access token in the body of the request. In addition to the expired access token, the client must provide a refresh token through a cookie that the server set during the initial authentication flow. The cookie that stores the refresh token is secured so that it cannot be stolen via JavaScript based attacks.

One of the great benefits of having a dedicated API client class is that the refresh logic does not need to be known outside this class. The rest of the application can send requests normally, and whenever a token refresh is necessary, the process can be handled internally by the class without affecting the rest of the application. Using pseudocode, the request sending logic can be expanded to support transparent token refreshes as follows:

  • Send the request
  • If the response is not 401
  • Return response to caller
  • Else
  • Refresh access token
  • Send original request again with new access token
  • Return response to caller

To prevent the code in the request() method of the API client from getting too complicated, the refresh logic can be implemented in a separate method. Start by renaming request() to requestInternal(), and creating a new request() wrapper method that calls it.

src/MicroblogApiClient.js: Request wrapper method

export default class MicroblogApiClient {
  async request(options) {
    let response = await this.requestInternal(options);
    return response;
  }

  async requestInternal(options) {
    ... // <-- original request() code here
  }

  ... // <-- no changes to other methods
}

The token refresh requests that are sent to Microblog API must include the refresh token cookie that the API set when the user first authenticated. The fetch() function used to make the requests does not send cookies by default, so it needs to be told to do it for this request by adding the credentials option. The change to add this option in the requestInternal() method is shown in the listing below.

src/MicroblogApiClient.js: Include cookies in requests

      response = await fetch(this.base_url + options.url + query, {
        method: options.method,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ' + localStorage.getItem('accessToken'),
          ...options.headers,
        },
        credentials: options.url === '/tokens' ? 'include' : 'omit',
        body: options.body ? JSON.stringify(options.body) : null,
      });
}

The credentials option is set to include only when the URL is that of the refresh token endpoint. In all other cases it is best to avoid sending unnecessary cookies by setting this option to omit.

Now the new request() wrapper method can be expanded to refresh the token and retry the original request when necessary.

src/MicroblogApiClient.js: Refresh token logic

export default class MicroblogApiClient {
  async request(options) {
    let response = await this.requestInternal(options);
    if (response.status === 401 && options.url !== '/tokens') {
      const refreshResponse = await this.put('/tokens', {
        access_token: localStorage.getItem('accessToken'),
      });
      if (refreshResponse.ok) {
        localStorage.setItem('accessToken', refreshResponse.body.access_token);
        response = await this.requestInternal(options);
      }
    }
    return response;
  }

  ... // <-- no changes to other methods
}

The refresh logic is only used when the original request comes back with a 401 status code, and the URL is not the one for the refresh token endpoint already. Checking the URL is useful to avoid a possible endless loop if the refresh token endpoint also fails with a 401 status code, maybe due to an expired or missing refresh token.

To refresh the token, a PUT request to /api/tokens is sent with the expired access token in the body. The cookie previously set during login will be included, per the credentials option added in requestInternal().

If the token refresh endpoint succeeds, then a new access token is returned. This token is written to local storage, replacing the now expired one. For added security, Microblog API will also send a new refresh token and invalidate the one that was just used, but this is in a secure cookie that is handled automatically by the browser.

Once the client is updated with the new access token, the original request can be retried, and this time the response is directly returned, regardless of its status code.

With these changes, a logged-in user should be able to stay on the system for as long as it wants, regardless of access token expirations.

Chapter Summary

  • The logged-in user can be shared with the application through a context and custom hook.
  • For common route behaviors such as redirecting to a login form or restricting access to logged-in users, create a wrapper component.
  • Abstract the authentication logic in the methods of an API client, so that its complexity does not bleed into other parts of the application.
  • Never render contents directly to the page with DOM APIs, as this introduces a risk of XSS attacks.
Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

21 comments
  • #1 Erick said

    Hello. I am new in react and this is a great blog! Thank you very much.
    How can I change the pattern of MicroblogApiClient.js (or similar file) to use a jwt token pair (access and refresh) authentication ?

  • #2 Miguel Grinberg said

    @Erick: the client application is not concerned with the structure or format of the token. From the client the token is just a string. The decision to use JWT or other format for the token is made by the server. The client does not need to change when the token format changes.

  • #3 DavidE said

    Thank you - this series is an amazing addition to your great Flask tutorial.

    If I log out from a specific page when I log back in I get redirected to the same page - is there a way to reset the location.state.next to be the default route?

  • #4 Miguel Grinberg said

    @DavidE: You can add a redirect to / in the logout function to reset the current page to the root.

  • #5 Abhishek said

    You can also use Outlet components from React Router Dom package to make code easier:
    Example code in PrivateRoute.jsx

    import { useLocation, Navigate, Outlet } from "react-router-dom";
    import { useUser } from "../contexts/UserProvider";
    
    export default function PrivateRoute({ children }) {
      const { user } = useUser();
      const location = useLocation();
    
      if (user === undefined) {
        return null;
      } else if (user) {
        // return children;
        return <Outlet />;
      } else {
        const url = location.pathname + location.search + location.hash;
        return <Navigate to="/login" state={{ next: url }} />;
      }
    }
    

    Example code in App.jsx

    ...
          <BrowserRouter>
            <FlashProvider>
              <ApiProvider>
                <UserProvider>
                  <Header />
                  <Routes>
                    <Route element={<PublicRoute />}>
                      <Route path="/login" element={<LoginPage />} />
                      <Route path="/register" element={<RegistrationPage />} />
                    </Route>
                    <Route element={<PrivateRoute />}>
                      <Route path="/" element={<FeedPage />} />
                      <Route path="/explore" element={<ExplorePage />} />
                      <Route path="/user/:username" element={<UserPage />} />
                      <Route path="*" element={<Navigate to="/" />} />
                    </Route>
                  </Routes>
                </UserProvider>
              </ApiProvider>
            </FlashProvider>
          </BrowserRouter>
    ...
    
  • #6 Miguel Grinberg said

    @Abhishek: I don't see how this change makes the code easier, but sure, it is another option you can use if you like it.

  • #7 JM said

    Hello Miguel,

    It seems like btoa/atob have been marked as deprecated: https://nodejs.org/docs/latest-v16.x/api/buffer.html#bufferatobdata. I can imagine it is not a big deal, as you would have already mentioned it if it was; nonetheless, do you think we should use the recommended Buffer.from(str, 'base64') instead?

    Kind regards,

  • #8 Miguel Grinberg said

    @JM: This is incorrect. You are looking at a deprecation notice for the Node.js functions btoa and atob, but this application runs in the browser, not under Node. The functions in the browser are not deprecated, and I doubt they'll ever be.

  • #9 dAresDeFi said

    My fetch requests are, as you outlined, made through the Api() context containing the following header:

    headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + localStorage.getItem('accessToken'),
    ...options.headers,
    }

    My flask standard token auth wrapper is as follows:
    def token_required(f):

    @wraps(f)
    def decorator(*args, **kwargs):
    
        token = None
    
        if "Authorization" in request.headers:
            token = request.headers["Authorization"]
               .... etc
    

    However, my fetch requests cannot get past @token_required routes ex:
    @app.route('/api/users/me', methods=['GET'])
    @token_required
    def me(token):
    """Retrieve the authenticated user"""

    #return token.current_user()
    return {"message":"token passed"}
    

    After some Postman tests, it seems my flask doesn't like the "Bearer" in front of the token in our Authorization header.

    For example, this request goes through successfully:

    const response = await api.get('/api/users/me',
    {
    headers: {
    Authorization: localStorage.getItem('accessToken')
    }
    });

    However, if I remove the "Bearer" from our Api Client request Header, the react app turns completely blank.

    Any ideas why my flask doesn't like requests with the "Bearer" in the Authorization?

  • #10 Miguel Grinberg said

    @dAresDeFi: You are asking me a question about your own code. I think it is dangerous that you are using security code that you don't fully understand, but in any case, the value of request['authorization'] is Bearer token. If you want to only use the token, then you have to parse the value of the header to extract this part.

  • #11 muhammad nasr said

    Hi Miguel,
    Thank you for React blog.
    I have a misunderstanding.

    In logout function I didn't notice access token in the headers, and I see it is required in the backend api.

    and for refresh token,
    which part stores the token or it is stored in cookies by backend?

  • #12 Miguel Grinberg said

    @muhammad: The logout() method sends a DELETE request through the request() method of the client class. This method inserts the access token. The refresh token is stored in a http-only cookie by the server for greater security. This is all covered in this article.

  • #13 Travis P said

    Hi Miguel,

    I noticed some strange behaviour with the Alert component / flash function on the login screen when providing incorrect credentials, and I compared this blog post against the code at the relevant tag in your Github repository. I see in your Github version of the FlashMessage component, you've passed the "show" prop to the Alert component, but you didn't do that in this blog. Without adding that prop, the alert never appears again after being manually dismissed. Could you elaborate on why that is the case? And did you add it to Github after writing this blog due to some changes in the library that required this prop?

    Another observation regarding the alert: If the alert is flashed due to incorrect password, and then you subsequently log in with the correct password, the alert for incorrect credentials still remains after redirect. Is there a way you would recommend dismissing this alert prematurely upon successful login?

    Thank you for the great tutorial series!

  • #14 Miguel Grinberg said

    @Travis: as always, if the text of the tutorial and the code differ, you should assume that the code is more up to date. I regularly update the dependencies and fix minor issues that appear as a result. Regarding your second question, the Flash context exports a hideFlash() function. You can call this function upon a successful login to dismiss any alert that may be visible.

  • #15 Alberto said

    Thanks a lot for this great tutorial!

    I have a doubt, the useEffect function that set the value of the user, inside UserProvider.js, runs only once after the first render of the Private Route component?

    I'm testing the auth logic with out refreshing token, so i expected that after the token expiration, the app redirected to login page, but instead it continuous inside the pages app and not update / set the user data to null. I must reload the page, to the user changes it's value to null, after check token expiration with the Flask End Point /me.

    Thanks Again.

  • #16 Miguel Grinberg said

    @Alberto: the useEffect function that you referenced has [api] as dependencies. That means that it will run on first render, and any time the api variable changes. If you want to force this function to run again, you have to ensure that when the token expires the api instance is regenerated to refresh the new user status as not authenticated. Then the effect function will run and reset the user to null.

  • #17 Alberto said

    [api] is the second argument of the useEffect Function, that comes from the MicroblogApiClient instance, implemented through the ApiProvider (ApiContext).

    When you say "...and any time the api variable changes..." It means that any call to the Flask Back End (User Info, List of User Posts, etc...), using the the MicroblogApiClient, will cause that the api variable changes is value and forcing to run the useEffect function?

    Or the App is not designed to force this function to run again, just when the token expires and not refreshing automatically the new user status as not authenticated? In this scenario, the app will continue working inside Private Route components, even thought the token has expired, until manual page refreshing.

  • #18 Miguel Grinberg said

    @Alberto: All I'm saying is that you are trying to use a different authentication flow than the one I show in this tutorial. You can't expect that the code will automatically adapt to your new flow, this is something that you need to plan for. If you want the PrivateRoute component to redirect to the login page when the token expires, then you should make sure to call setUser(null) when you detect an expired token, as that is what PrivateRoute checks to decide if a redirect to login is needed.

  • #19 Alberto said

    It's true, i'm using a different authetication flow. I just implemented a first adapt approach:
    All the API calls to my Back End, are redirected to a Middleware (Back End) that verify Token validation.
    If no token is providen, the back end response status is 403.
    Else if token is providen, but has expired or has some mistake, the back end response status is 401.
    *Else if token is providen and succesfully validated, the back end redirect to the function that implement the logic of the original API call, and the response status is 200, attached with the metadata of the second function.

    *If React Front End check that the repsonse status is different from 200, so it calls the logout function, redirecting the user to the login page.

    I have to adapt the logic, for the app be able to check the token expiration for every time the routes changes.

    Thanks a lot Miguel.

    first passchecks for Token validation

  • #20 Edward Iniko said

    Maybe I'm wrong but I am expecting the 'api/me' endpoint should have an authorization header with token to retrieve a specific user from the db

  • #21 Miguel Grinberg said

    @Edward: That is correct. What did you see that does not agree with your statement?

Leave a Comment