DEV Community

loading...
Cover image for React authentication, simplified

React authentication, simplified

tyrw profile image Tyler Warnock Originally published at userfront.com ・10 min read

Authentication is one of those things that just always seems to take a lot more effort than we want it to.

To set up auth, you have to re-research topics you haven’t thought about since the last time you did authentication, and the fast-paced nature of the space means things have often changed in the meantime. New threats, new options, and new updates may have kept you guessing and digging through docs in your past projects.

In this article, we lay out a different approach to authentication (plus access control & SSO) in React applications. Rather than add a static library that you have to keep up to date or re-research each time you want to implement auth, we’ll use a service that stays up to date automatically and is a much simpler alternative to Auth0, Okta, and others.

React authentication

We typically use a similar approach when writing authentication in React: our React app makes a request to our authentication server, which then returns an access token. That token is saved in the browser and can be used in subsequent requests to your server (or other servers, if needed). Whether writing standard email & password authentication or using magic links or single sign on (SSO) logins like Google, Azure, or Facebook, we want our React app to send an initial request to an authentication server and have that server handle all the complexity of generating a token.

So React’s responsibility in authentication is to:

  1. Send the initial request to the authentication server
  2. Receive and store the access token
  3. Send the access token to your server with each subsequent request

JWT access tokens

JSON Web Tokens (JWTs) are compact, URL-safe tokens that can be used for authentication and access control in React applications. Each JWT has a simple JSON-object as its “payload” and is signed such that your server can verify that the payload is authentic. An example JWT would look like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9.f7iKN-xi24qrQ5NQtOe0jiriotT-rve3ru6sskbQXnA
Enter fullscreen mode Exit fullscreen mode

The payload for this token is the middle section (separated by periods):

eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9
Enter fullscreen mode Exit fullscreen mode

The JWT payload can be decoded from base64 to yield the JSON object:

JSON.parse(atob("eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9"));

// =>
{
  userId: 1,
  authorization: admin
}
Enter fullscreen mode Exit fullscreen mode

It’s important to note that this payload is readable by anyone with the JWT, including your React application or a third party.

Anyone that has the JWT can read its contents. However, only the authentication server can generate valid JWTs -- your React application, your application server, or a malicious third party cannot generate valid JWTs. So in addition to reading the JWT, your server also needs to verify the JWT as authentic by checking it against a public key. This allows your application server to verify incoming JWTs and reject any tokens that were not created by the authentication server or that have expired.

The flow for using a JWT in a React application looks like this:

  1. Your React app requests a JWT whenever the user wants to sign on.
  2. The authentication server generates a JWT using a private key and then sends the JWT back to your React app.
  3. Your React app stores this JWT and sends it to your application server whenever your user needs to make a request.
  4. Your application server verifies the JWT using a public key and then read the payload to determine which user is making the request.

Each of these steps is simple to write down, but each step has its own pitfalls when you actually want to implement it and keep it secure. Especially over time, as new threat vectors emerge and new platforms need to be patched or supported, the security overhead can add up quickly.

Userfront removes auth complexity in React apps

Userfront is a framework that abstracts away auth complexity. This makes it much easier for you to work with authentication in a React application and, perhaps most importantly, keeps all the auth protocols updated for you automatically over time.

The underlying philosophy with Userfront is that world-class auth should not take effort – it should be easy to set up, and security updates should happen for you automatically. Userfront has all the bells and whistles of authentication, Single Sign On (SSO), access control, and multi-tenancy, with a production-ready free tier up to 10,000 monthly active users. For most modern React applications, it’s a great solution.

Setting up authentication in React

Now we will go through building all the main aspects of authentication in a React application. The final code for this example is available here.

Use your favorite boilerplate to set up your React application and get your build pipeline in order. In this article, we’ll use Create React App, which does a lot of the setup work for us, and we’ll also add React Router for our client-side routing. Start by installing Create React App and React Router:

npx create-react-app my-app
cd my-app
npm install react-router-dom --save
npm start
Enter fullscreen mode Exit fullscreen mode

Now our React application is available at http://localhost:3000

Create React App authentication

Just like it says, we can now edit the src/App.js file to start working.

Replace the contents of src/App.js with the following, based on the React Router quickstart:

// src/App.js

import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/login">Login</Link>
            </li>
            <li>
              <Link to="/reset">Reset</Link>
            </li>
            <li>
              <Link to="/dashboard">Dashboard</Link>
            </li>
          </ul>
        </nav>

        <Switch>
          <Route path="/login">
            <Login />
          </Route>
          <Route path="/reset">
            <PasswordReset />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function Login() {
  return <h2>Login</h2>;
}

function PasswordReset() {
  return <h2>Password Reset</h2>;
}

function Dashboard() {
  return <h2>Dashboard</h2>;
}
Enter fullscreen mode Exit fullscreen mode

Now we have a very simple app with routing:

Route Description
/ Home page
/login Login page
/reset Password reset page
/dashboard User dashboard, for logged in users only

React Router authentication

This is all the structure we need to start adding authentication.

Signup, login, and password reset with Userfront

First, create a Userfront account at https://userfront.com. This will give you a signup form, login form, and password reset form you can use for the next steps.

In the Toolkit section of your Userfront dashboard, you can find the instructions for installing your signup form:

Userfront installation instructions

Follow the instructions by installing the Userfront react package with:

npm install @userfront/react --save
npm start
Enter fullscreen mode Exit fullscreen mode

Then add the form to your home page by importing and initializing Userfront, and then updating the Home() function to render the form.

// src/App.js

import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Userfront from "@userfront/react";

Userfront.init("demo1234");

const SignupForm = Userfront.build({
  toolId: "nkmbbm",
});

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/login">Login</Link>
            </li>
            <li>
              <Link to="/reset">Reset</Link>
            </li>
            <li>
              <Link to="/dashboard">Dashboard</Link>
            </li>
          </ul>
        </nav>

        <Switch>
          <Route path="/login">
            <Login />
          </Route>
          <Route path="/reset">
            <PasswordReset />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
      <SignupForm />
    </div>
  );
}

function Login() {
  return <h2>Login</h2>;
}

function PasswordReset() {
  return <h2>Password Reset</h2>;
}

function Dashboard() {
  return <h2>Dashboard</h2>;
}
Enter fullscreen mode Exit fullscreen mode

Now the home page has your signup form. Try signing up a user:

React signup form

The form is in "Test mode" by default, which will create user records in a test environment you can view separately in your Userfront dashboard:

Userfront test mode

Continue by adding your login and password reset forms in the same way that you added your signup form:

// src/App.js

import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Userfront from "@userfront/react";

Userfront.init("demo1234");

const SignupForm = Userfront.build({
  toolId: "nkmbbm",
});
const LoginForm = Userfront.build({
  toolId: "alnkkd",
});
const PasswordResetForm = Userfront.build({
  toolId: "dkbmmo",
});

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/login">Login</Link>
            </li>
            <li>
              <Link to="/reset">Reset</Link>
            </li>
            <li>
              <Link to="/dashboard">Dashboard</Link>
            </li>
          </ul>
        </nav>

        <Switch>
          <Route path="/login">
            <Login />
          </Route>
          <Route path="/reset">
            <PasswordReset />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
      <SignupForm />
    </div>
  );
}

function Login() {
  return (
    <div>
      <h2>Login</h2>
      <LoginForm />
    </div>
  );
}

function PasswordReset() {
  return (
    <div>
      <h2>Password Reset</h2>
      <PasswordResetForm />
    </div>
  );
}

function Dashboard() {
  return <h2>Dashboard</h2>;
}
Enter fullscreen mode Exit fullscreen mode

At this point, your signup, login, and password reset should all be functional.

Your users can sign up, log in, and reset their password.

React signup, login, password reset

Protected route in React

Usually, we don't want users to be able to view the dashboard unless they are logged in. This is known as protecting a route.

Whenever a user is not logged in but tries to visit /dashboard, we can redirect them to the login screen.

We can accomplish this by updating the Dashboard component in src/App.js to handle the conditional logic.

When a user is logged in with Userfront, they will have an access token available as Userfront.accessToken(). We can check for this token to determine if the user is logged in.

Add the Redirect component to the import statement for React Router, and then update the Dashboard component to redirect if no access token is present.

// src/App.js

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect, // Be sure to add this import
} from "react-router-dom";

// ...

function Dashboard() {
  function renderFn({ location }) {
    // If the user is not logged in, redirect to login
    if (!Userfront.accessToken()) {
      return (
        <Redirect
          to={{
            pathname: "/login",
            state: { from: location },
          }}
        />
      );
    }

    // If the user is logged in, show the dashboard
    const userData = JSON.stringify(Userfront.user, null, 2);
    return (
      <div>
        <h2>Dashboard</h2>
        <pre>{userData}</pre>
        <button onClick={Userfront.logout}>Logout</button>
      </div>
    );
  }

  return <Route render={renderFn} />;
}
Enter fullscreen mode Exit fullscreen mode

Notice also that we've added a logout button by calling Userfront.logout() directly:

<button onClick={Userfront.logout}>Logout</button>
Enter fullscreen mode Exit fullscreen mode

Now, when a user is logged in, they can view the dashboard. If the user is not logged in, they will be redirected to the login page.

React protected route

React authentication with an API

You will probably want to retrieve user-specific information from your backend. In order to protect these API endpoints, your server should check that incoming JWTs are valid.

There are many libraries to read and verify JWTs across various languages; here are a few popular libraries for handling JWTs:

For Userfront, the access token is available in your React application as Userfront.accessToken().

Your React application can send this as a Bearer token inside the Authorization header. For example:

// Example of calling an endpoint with a JWT

async function getInfo() {
  const res = await window.fetch("/your-endpoint", {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${Userfront.accessToken()}`,
    },
  });

  console.log(res);
}

getInfo();
Enter fullscreen mode Exit fullscreen mode

To handle a request like this, your backend should read the JWT from the Authorization header and verify that it is valid using the public key found in your Userfront dashboard.

Here is an example of Node.js middleware to read and verify the JWT:

// Node.js example (Express.js)

const jwt = require("jsonwebtoken");

function authenticateToken(req, res, next) {
  // Read the JWT access token from the request header
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];
  if (token == null) return res.sendStatus(401); // Return 401 if no token

  // Verify the token using the Userfront public key
  jwt.verify(token, process.env.USERFRONT_PUBLIC_KEY, (err, auth) => {
    if (err) return res.sendStatus(403); // Return 403 if there is an error verifying
    req.auth = auth;
    next();
  });
}
Enter fullscreen mode Exit fullscreen mode

Using this approach, any invalid or missing tokens would be rejected by your server. You can also reference the contents of the token later in the route handlers using the req.auth object:

console.log(req.auth);

// =>
{
  mode: 'test',
  tenantId: 'demo1234',
  userId: 1,
  userUuid: 'ab53dbdc-bb1a-4d4d-9edf-683a6ca3f609',
  isConfirmed: false,
  authorization: {
    demo1234: {
      tenantId: 'demo1234',
      name: 'Demo project',
      roles: ["admin"],
      permissions: []
    },
  },
  sessionId: '35d0bf4a-912c-4429-9886-cd65a4844a4f',
  iat: 1614114057,
  exp: 1616706057
}
Enter fullscreen mode Exit fullscreen mode

With this information, you can perform further checks as desired, or use the userId or userUuid to look up user information to return.

For example, if you wanted to limit a route to admin users, you could check against the authorization object from the verified access token:

// Node.js example (Express.js)

app.get("/users", (req, res) => {
  const authorization = req.auth.authorization["demo1234"] || {};

  if (authorization.roles.includes("admin")) {
    // Allow access
  } else {
    // Deny access
  }
});
Enter fullscreen mode Exit fullscreen mode

React SSO (Single Sign On)

From here, you can add social identity providers like Google, Facebook, and LinkedIn to your React application, or business identity providers like Azure AD, Office365, and more.

You do this by creating an application with the identity provider (e.g. Google), and then adding that application's credentials to the Userfront dashboard. The result is a modified sign on experience:

React SSO form

No additional code is needed to implement Single Sign On using this approach: you can add and remove providers without updating your forms or the way you handle JWTs.

Final notes

Adding authentication and access control to your React application doesn't have to be a hassle. Both the setup step and, more importantly, the maintenance over time, are handled with modern platforms like Userfront.

JSON Web Tokens allow you to cleanly separate your auth token generation layer from the rest of your application, making it easier to reason about and more modular for future needs. This architecture also allows you to focus your efforts on your core application, where you are likely to create much more value for yourself or your clients.

For more details on adding auth to your React application, visit the Userfront guide, which covers everything from setting up your auth forms to API documentation, example repositories, working with different languages and frameworks, and more.

Create a free Userfront project

Discussion (17)

Collapse
wparad profile image
Warren Parad • Edited

Need to be careful of a couple of things:

  • Article suggests using the Auth0 jsonwebtoken library, but this one doesn't actually support all the necessary token types
  • Specifically it seams that UserFront creates RSA tokens, when this is no longer the best solution. It may be good to check out the alternatives to see if one of the standard SaaS solutions does provide this
  • The JWT in the article isn't a valid access token according to the openId specification. This is pretty common problem which makes it impossible for clients and users to integrate effectively. It should look something like this:
{
  "iss": "https://authress.io",
  "sub": "user_100822687410662214374",
  "iat": 1620125048,
  "exp": 1620211448,
  "scope": "openid profile email",
  "azp": "api.authress.io",
  "client_id": "9fe39cec",
  "aud": [
    "accounts.api.authress.io"
  ]
}
Enter fullscreen mode Exit fullscreen mode
Collapse
tyrw profile image
Tyler Warnock Author • Edited

Thanks for your thoughtful response @wparad !

For context, we are building Userfront for simplicity and usability in the vast majority of use cases. In doing so, we use defaults like userId instead of sub so that it makes sense to most developers without having to delve into confusing terminology. We will eventually introduce the ability to format tokens to different standards for specific use cases like you mentioned, but for now we present it as simply as possible. It's worth pointing out that all are valid JWTs according to the JWT specification.

Would love to hear more about your comments on the jsonwebtoken library. It is easily the most popular library and why we recommend it. What do you not like about it?

For RSA, that is the NSA's recommendation, so that's what we use. You'll have a tough time convincing us otherwise!

Collapse
codedpills profile image
Zak

Hi Tyler,

I just chanced on this and I must say it's a really great article.

Userfront seems really useful for most of the common use cases. I have not yet had time to delve deeply into the docs but I was wondering if it's possible to customise the auth forms that's provided by Userfront or perhaps integrate with your own custom forms?

Thanks

Collapse
tyrw profile image
Tyler Warnock Author • Edited

Thanks @codedpills , and yes you can build your own forms using the Userfront Core JS library (it's how the toolkit forms are built). We'll have a how-to about that shortly, and I'll let you know 👍

Collapse
matfrana profile image
Matteo Frana

Hi Tyler,
great article and it seems a great product.

The 10.000 monthly active users means that if I have 1.000 users they will be able to login as many times as they like and I will be always in the free tier?
I'd like to use your solution for two upcoming projects. In one case the customer needs to have all the data in the EU for their privacy policy: is there the possibility to choose a EU zone for the data storage?

Thank you,
Matteo

Collapse
tyrw profile image
Tyler Warnock Author • Edited

Hi @matfrana ,

Yes that's absolutely what it means -- we want to offer free auth for small projects, and in general we want best-in-class authentication to be free for any project that needs it, because that will make the internet better. 1,000 users is no sweat for our infrastructure, so yes it is and will be free.

For your EU question: we don't currently offer the ability to store all data in a given location, and it's currently housed primarily in the US. Your request is a common one though, so we plan to add it in the future. We'd love to support your needs however we can.

Thank you for asking!

Collapse
marcolino profile image
Marcolino

But, according to Userfront terms of use, will I really own my users authentication data? And what if Userfront should fail, or be sold to some Micro$oft? Is it possible - at least in principle - to host a copy of Userfront on premises?

Collapse
tyrw profile image
Tyler Warnock Author

Thanks @marcolino for asking!

We also don't like vendor lock in, so we made it easy to export your user data to CSV, and, for things like SSO, to set the auth up with your own client IDs and secrets. That way if your needs change, you can always migrate.

We don't currently offer it on premises, as much of our value comes from keeping things up to date automatically for you. That said, if there is demand for it in the future, we are open to it.

Collapse
kennymanman profile image
kennymanman

Hey, want to try using userfront with an app and webpage. I'm new to backend and wanted to know how to gain access to a specific page that isnt in the navbar and also if i should use the frontend or backend forms to login customers and display their specific information.

Collapse
tyrw profile image
Tyler Warnock Author

Hi @kennymanman , a user is considered "logged in" if they have a JWT access token. So in general you have 2 options: check for the token in the browser or check for the token on the server.

If you check in the browser, you don't need a backend, but a savvy user could disable JavaScript (which would disable your check), and still view the page. So this approach should only be used for things that aren't sensitive.

If you check on your server, you can choose to never send the page in the first place, so a user couldn't view the page. The downside of this is of course that you need to have a backend server.

Do you have a sense for which one you would want? If it's the latter, what tech stack would you use for the backend?

Collapse
fmontenegro0510 profile image
Francisco

Awesome!! Thanks a lot for the guide.

Collapse
tyrw profile image
Collapse
codehan25 profile image
Codehan25

Thanks for this very detailed and informative article. I think I‘ll give it a try.

Collapse
sm0ke profile image
Sm0ke

Userfront looks good. Ty!

Collapse
tyrw profile image
Collapse
dev_emmy profile image
nshimiye_emmy

wow! great article.

Collapse
tyrw profile image
Forem Open with the Forem app