React Typescript Authentication example with Hooks

In this tutorial, we’re gonna build a React Typescript: Authentication and Authorization example with React Hooks, React Router, Axios and Bootstrap (without Redux). I will show you:

  • JWT Authentication Flow for User Signup & User Login
  • Project Structure for React Typescript Authentication (without Redux) with React Router & Axios
  • Creating React Components with Form Validation using Formik and Yup
  • React Typescript Components for accessing protected Resources (Authorization)
  • Dynamic Navigation Bar in React Typescript App

Related Posts:
In-depth Introduction to JWT-JSON Web Token
React Typescript with API call example using Hooks and Axios

Fullstack (JWT Authentication & Authorization example):
React + Spring Boot
React + Node Express + MySQL/PostgreSQL
React + Node Express + MongoDB

React Components instead:
React Typescript Login and Registration example

Overview of React Typescript Authentication example

We will build a React Typescript Authentication and Authorization application in that:

  • There are Login/Logout, Signup pages.
  • Form data will be validated by front-end before being sent to back-end.
  • Depending on User’s roles (admin, moderator, user), Navigation Bar changes its items automatically.

Here are the screenshots:
– Signup Page:

react-typescript-authentication-example-signup

– Form Validation Support:

react-typescript-authentication-example-form-validation

– Login Page:

react-typescript-authentication-example-login

– Profile Page (for successful Login):

react-typescript-authentication-example-profile-page

– For Moderator account login, the navigation bar will change by authorities:

react-typescript-authentication-example-authorization-login

– Check Browser Local Storage:

react-typescript-authentication-example-localstorage

– Try to access unauthorized resource (Admin Page):

react-typescript-authentication-example-unauthorization

If you want to add refresh token, please visit:
React Refresh Token with JWT and Axios Interceptors

User Registration and User Login Flow

For JWT Authentication, we’re gonna call 2 endpoints:

  • POST api/auth/signup for User Registration
  • POST api/auth/signin for User Login

The following flow shows you an overview of Requests and Responses that React Typescript Authentication Client will make or receive from Auth Server. This Client must add a JWT to HTTP Header before sending request to protected resources.

react-typescript-authentication-example-flow

You can find step by step to implement these back-end servers in following tutorial:

Demo Video

This is full React + Node Express JWT Authentication & Authorization demo (with form validation, check signup username/email duplicates, test authorization with 3 roles: Admin, Moderator, User):

And this is using Spring Boot Server:

In the videos above, we use React with Javascript and Class Component. But the UI and logic and are the same as the React Typescript project in this tutorial.

React Typescript Authentication Component Diagram

Let’s look at the diagram below.

react-typescript-authentication-example-overview

– The App component is a container with React Router (BrowserRouter). Basing on the state, the navbar can display its items.

Login & Register components have form for data submission (with support of formik and yup library). They call methods from auth.service to make login/register request.

auth.service uses axios to make HTTP requests. Its also store or get JWT from Browser Local Storage inside these methods.

Home component is public for all visitor.

Profile component displays user information after the login action is successful.

BoardUser, BoardModerator, BoardAdmin components will be displayed by state user.roles. In these components, we use user.service to access protected resources from Web API.

user.service uses auth-header() helper function to add JWT to HTTP header. auth-header() returns an object containing the JWT of the currently logged in user from Local Storage.

Technology

We’re gonna use these modules:

  • React 17/16
  • typescript 4.3.5
  • react-router-dom 5.2.0
  • axios 0.21.1
  • formik 2.2.9
  • Bootstrap 4
  • yup 0.32.9

Project Structure

This is folders & files structure for this React Typescript Authenticaion application:

react-typescript-authentication-project-structure

With the explanation in diagram above, you can understand the project structure easily.

Additionally, EventBus is for emitting Logout event when the Token is expired.

Setup React Typescript Authentication Project

Open cmd at the folder you want to save Project folder, run command:
npx create-react-app react-typescript-authentication-example --template typescript

Import Bootstrap to React Typescript Project

Run command:
yarn add [email protected]
– Or: npm install [email protected].

Open src/App.tsx and modify the code inside it as following-

...
import "bootstrap/dist/css/bootstrap.min.css";

const App: React.FC = () => {
  return (
    // ...
  );
}

export default App;

Add React Router to React Typescript Authentication Project

When using Typescript with React, we don’t use Proptypes. Typescript is stronger than Propstypes.

npm has many dependencies with prefix @types/{name} such as @types/lodash, @types/react… which is easy to install and use. For this project, we use @types/react-router-dom.

– Run the command: yarn add react-router-dom @types/react-router-dom.
Or: npm install react-router-dom @types/react-router-dom.

– Open src/index.tsx and wrap App component by BrowserRouter object.

import ReactDOM from 'react-dom';
import { BrowserRouter } from "react-router-dom";

import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

reportWebVitals();

Create Services

We’re gonna create two services in src/services folder:


services

auth-header.ts

auth.service.ts (Authentication service)

user.service.ts (Data service)


Before working with these services, we need to install Axios with command:
yarn add axios or npm install axios

Authentication service

The service uses Axios for HTTP requests and Local Storage for user information & JWT.
It provides following important methods:

  • login(): POST {username, password} & save JWT to Local Storage
  • logout(): remove JWT from Local Storage
  • register(): POST {username, email, password}
  • getCurrentUser(): get stored user information (including JWT)
import axios from "axios";

const API_URL = "http://localhost:8080/api/auth/";

export const register = (username: string, email: string, password: string) => {
  return axios.post(API_URL + "signup", {
    username,
    email,
    password,
  });
};

export const login = (username: string, password: string) => {
  return axios
    .post(API_URL + "signin", {
      username,
      password,
    })
    .then((response) => {
      if (response.data.accessToken) {
        localStorage.setItem("user", JSON.stringify(response.data));
      }

      return response.data;
    });
};

export const logout = () => {
  localStorage.removeItem("user");
};

export const getCurrentUser = () => {
  const userStr = localStorage.getItem("user");
  if (userStr) return JSON.parse(userStr);

  return null;
};

Data service

We also have methods for retrieving data from server. In the case we access protected resources, the HTTP request needs Authorization header.

Let’s create a helper function called authHeader() inside auth-header.ts:

export default function authHeader() {
  const userStr = localStorage.getItem("user");
  let user = null;
  if (userStr)
    user = JSON.parse(userStr);

  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken };
  } else {
    return {};
  }
}

The code above checks Local Storage for user item. If there is a logged in user with accessToken (JWT), return HTTP Authorization header. Otherwise, return an empty object.


Note: For Node Express back-end, please use x-access-token header like this:

export default function authHeader() {
  const userStr = localStorage.getItem("user");
  let user = null;
  if (userStr)
    user = JSON.parse(userStr);

  if (user && user.accessToken) {
    // return { Authorization: 'Bearer ' + user.accessToken }; // for Spring Boot back-end
    return { 'x-access-token': user.accessToken };       // for Node Express back-end
  } else {
    return {};
  }
}

Now we define a service for accessing data in user.service.ts:

import axios from "axios";
import authHeader from "./auth-header";

const API_URL = "http://localhost:8080/api/test/";

export const getPublicContent = () => {
  return axios.get(API_URL + "all");
};

export const getUserBoard = () => {
  return axios.get(API_URL + "user", { headers: authHeader() });
};

export const getModeratorBoard = () => {
  return axios.get(API_URL + "mod", { headers: authHeader() });
};

export const getAdminBoard = () => {
  return axios.get(API_URL + "admin", { headers: authHeader() });
};

You can see that we add a HTTP header with the help of authHeader() function when requesting authorized resource.

Create React Typescript Components for Authentication

In src folder, create new folder named components and add several files as following:


components

Login.tsx

Register.tsx

Profile.tsx


React Typescript Form Validation overview

Now we need a library for Form validation, so we’re gonna add formik and yup library to our project.

Run the command: yarn add formik yup
Or: npm install formik yup

Import following items:

import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";

This is how we put them in React Component with 3 important attributes:

  • initialValues
  • validationSchema
  • onSubmit
const Login: React.FC<Props> = (...) => {
  ...

  const initialValues: {
    username: string;
    password: string;
  } = {
    username: "",
    password: "",
  };

  const validationSchema = Yup.object().shape({
    username: Yup.string().required("This field is required!"),
    password: Yup.string().required("This field is required!"),
  });

  const handleLogin = (formValue: { username: string; password: string }) => {
    const { username, password } = formValue;
    ...
  };

  return (
    <Formik
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={handleLogin}
    >
      <Form>
        <div>
          <label htmlFor="username">Password</label>
          <Field name="username" type="text" />
          <ErrorMessage name="username" component="div" />
        </div>

        <div>
          <label htmlFor="password">Password</label>
          <Field name="password" type="password" />
          <ErrorMessage name="password" component="div" />
        </div>

        <div>
          <button type="submit" disabled={loading}>
            Login
          </button>
        </div>
      </Form>
    </Formik>
  );
};

More details at:
React Form Validation example with Hooks, Formik and Yup

Login Page

This page has a Form with username & password.
– We’re gonna verify them as required field.
– If the verification is ok, we call AuthService.login() method, then direct user to Profile page: this.props.history.push("/profile");, or show message with response error.

Login.tsx

import React, { useState } from "react";
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";

import { login } from "../services/auth.service";
import { RouteComponentProps } from "react-router-dom";

interface RouterProps {
  history: string;
}

type Props = RouteComponentProps<RouterProps>;

const Login: React.FC<Props> = ({ history }) => {
  const [loading, setLoading] = useState<boolean>(false);
  const [message, setMessage] = useState<string>("");

  const initialValues: {
    username: string;
    password: string;
  } = {
    username: "",
    password: "",
  };

  const validationSchema = Yup.object().shape({
    username: Yup.string().required("This field is required!"),
    password: Yup.string().required("This field is required!"),
  });

  const handleLogin = (formValue: { username: string; password: string }) => {
    const { username, password } = formValue;

    setMessage("");
    setLoading(true);

    login(username, password).then(
      () => {
        history.push("/profile");
        window.location.reload();
      },
      (error) => {
        const resMessage =
          (error.response &&
            error.response.data &&
            error.response.data.message) ||
          error.message ||
          error.toString();

        setLoading(false);
        setMessage(resMessage);
      }
    );
  };

  return (
    <div className="col-md-12">
      <div className="card card-container">
        <img
          src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
          alt="profile-img"
          className="profile-img-card"
        />
        <Formik
          initialValues={initialValues}
          validationSchema={validationSchema}
          onSubmit={handleLogin}
        >
          <Form>
            <div className="form-group">
              <label htmlFor="username">Username</label>
              <Field name="username" type="text" className="form-control" />
              <ErrorMessage
                name="username"
                component="div"
                className="alert alert-danger"
              />
            </div>

            <div className="form-group">
              <label htmlFor="password">Password</label>
              <Field name="password" type="password" className="form-control" />
              <ErrorMessage
                name="password"
                component="div"
                className="alert alert-danger"
              />
            </div>

            <div className="form-group">
              <button type="submit" className="btn btn-primary btn-block" disabled={loading}>
                {loading && (
                  <span className="spinner-border spinner-border-sm"></span>
                )}
                <span>Login</span>
              </button>
            </div>

            {message && (
              <div className="form-group">
                <div className="alert alert-danger" role="alert">
                  {message}
                </div>
              </div>
            )}
          </Form>
        </Formik>
      </div>
    </div>
  );
};

export default Login;

Register Page

This page is similar to Login Page.

For Form Validation, there are some more details:

  • username: required, between 3 and 20 characters
  • email: required, email format
  • password: required, between 6 and 40 characters

We’re gonna call AuthService.register() method and show response message (successful or error).

Register.tsx

import React, { useState } from "react";
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";

import IUser from "../types/user.type";
import { register } from "../services/auth.service";

const Register: React.FC = () => {
  const [successful, setSuccessful] = useState<boolean>(false);
  const [message, setMessage] = useState<string>("");

  const initialValues: IUser = {
    username: "",
    email: "",
    password: "",
  };

  const validationSchema = Yup.object().shape({
    username: Yup.string()
      .test(
        "len",
        "The username must be between 3 and 20 characters.",
        (val: any) =>
          val &&
          val.toString().length >= 3 &&
          val.toString().length <= 20
      )
      .required("This field is required!"),
    email: Yup.string()
      .email("This is not a valid email.")
      .required("This field is required!"),
    password: Yup.string()
      .test(
        "len",
        "The password must be between 6 and 40 characters.",
        (val: any) =>
          val &&
          val.toString().length >= 6 &&
          val.toString().length <= 40
      )
      .required("This field is required!"),
  });

  const handleRegister = (formValue: IUser) => {
    const { username, email, password } = formValue;

    register(username, email, password).then(
      (response) => {
        setMessage(response.data.message);
        setSuccessful(true);
      },
      (error) => {
        const resMessage =
          (error.response &&
            error.response.data &&
            error.response.data.message) ||
          error.message ||
          error.toString();

        setMessage(resMessage);
        setSuccessful(false);
      }
    );
  };

  return (
    <div className="col-md-12">
      <div className="card card-container">
        <img
          src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
          alt="profile-img"
          className="profile-img-card"
        />
        <Formik
          initialValues={initialValues}
          validationSchema={validationSchema}
          onSubmit={handleRegister}
        >
          <Form>
            {!successful && (
              <div>
                <div className="form-group">
                  <label htmlFor="username"> Username </label>
                  <Field name="username" type="text" className="form-control" />
                  <ErrorMessage
                    name="username"
                    component="div"
                    className="alert alert-danger"
                  />
                </div>

                <div className="form-group">
                  <label htmlFor="email"> Email </label>
                  <Field name="email" type="email" className="form-control" />
                  <ErrorMessage
                    name="email"
                    component="div"
                    className="alert alert-danger"
                  />
                </div>

                <div className="form-group">
                  <label htmlFor="password"> Password </label>
                  <Field
                    name="password"
                    type="password"
                    className="form-control"
                  />
                  <ErrorMessage
                    name="password"
                    component="div"
                    className="alert alert-danger"
                  />
                </div>

                <div className="form-group">
                  <button type="submit" className="btn btn-primary btn-block">Sign Up</button>
                </div>
              </div>
            )}

            {message && (
              <div className="form-group">
                <div
                  className={
                    successful ? "alert alert-success" : "alert alert-danger"
                  }
                  role="alert"
                >
                  {message}
                </div>
              </div>
            )}
          </Form>
        </Formik>
      </div>
    </div>
  );
};

export default Register;

Profile Page

This page gets current User from Local Storage by calling AuthService.getCurrentUser() method and show user information (with token).

Profile.tsx

import React from "react";
import { getCurrentUser } from "../services/auth.service";

const Profile: React.FC = () => {
  const currentUser = getCurrentUser();

  return (
    <div className="container">
      <header className="jumbotron">
        <h3>
          <strong>{currentUser.username}</strong> Profile
        </h3>
      </header>
      <p>
        <strong>Token:</strong> {currentUser.accessToken.substring(0, 20)} ...{" "}
        {currentUser.accessToken.substr(currentUser.accessToken.length - 20)}
      </p>
      <p>
        <strong>Id:</strong> {currentUser.id}
      </p>
      <p>
        <strong>Email:</strong> {currentUser.email}
      </p>
      <strong>Authorities:</strong>
      <ul>
        {currentUser.roles &&
          currentUser.roles.map((role: string, index: number) => <li key={index}>{role}</li>)}
      </ul>
    </div>
  );
};

export default Profile;

Create React Typescript Components for accessing Resources

These components will use UserService to request data from API.


components

Home.tsx

BoardUser.tsx

BoardModerator.tsx

BoardAdmin.tsx


Home Page

This is a public page that shows public content. People don’t need to log in to view this page.

Home.tsx

import React, { useState, useEffect } from "react";

import { getPublicContent } from "../services/user.service";

const Home: React.FC = () => {
  const [content, setContent] = useState<string>("");

  useEffect(() => {
    getPublicContent().then(
      (response) => {
        setContent(response.data);
      },
      (error) => {
        const _content =
          (error.response && error.response.data) ||
          error.message ||
          error.toString();

        setContent(_content);
      }
    );
  }, []);

  return (
    <div className="container">
      <header className="jumbotron">
        <h3>{content}</h3>
      </header>
    </div>
  );
};

export default Home;

Role-based Pages

We’re gonna have 3 pages for accessing protected data:

  • BoardUser page calls UserService.getUserBoard()
  • BoardModerator page calls UserService.getModeratorBoard()
  • BoardAdmin page calls UserService.getAdminBoard()

I will show you User Page for example, other Pages are similar to this Page.

BoardUser.tsx

import React, { useState, useEffect } from "react";

import { getUserBoard } from "../services/user.service";

const BoardUser: React.FC = () => {
  const [content, setContent] = useState<string>("");

  useEffect(() => {
    getUserBoard().then(
      (response) => {
        setContent(response.data);
      },
      (error) => {
        const _content =
          (error.response &&
            error.response.data &&
            error.response.data.message) ||
          error.message ||
          error.toString();

        setContent(_content);
      }
    );
  }, []);

  return (
    <div className="container">
      <header className="jumbotron">
        <h3>{content}</h3>
      </header>
    </div>
  );
};

export default BoardUser;

You can simplify import statement with:
Absolute Import in React

Add Navbar and define Routes

Now we add a navigation bar in App component. This is the root container for our application.
The navbar dynamically changes by login status and current User’s roles.

  • Home: always
  • Login & Sign Up: if user hasn’t signed in yet
  • User: AuthService.getCurrentUser() returns a value
  • Board Moderator: roles includes ROLE_MODERATOR
  • Board Admin: roles includes ROLE_ADMIN

src/App.tsx

import React from "react";
import { useState, useEffect } from "react";
import { Switch, Route, Link } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";

import * as AuthService from "./services/auth.service";
import IUser from './types/user.type';

import Login from "./components/Login";
import Register from "./components/Register";
import Home from "./components/Home";
import Profile from "./components/Profile";
import BoardUser from "./components/BoardUser";
import BoardModerator from "./components/BoardModerator";
import BoardAdmin from "./components/BoardAdmin";

import EventBus from "./common/EventBus";

const App: React.FC = () => {
  const [showModeratorBoard, setShowModeratorBoard] = useState<boolean>(false);
  const [showAdminBoard, setShowAdminBoard] = useState<boolean>(false);
  const [currentUser, setCurrentUser] = useState<IUser | undefined>(undefined);

  useEffect(() => {
    const user = AuthService.getCurrentUser();

    if (user) {
      setCurrentUser(user);
      setShowModeratorBoard(user.roles.includes("ROLE_MODERATOR"));
      setShowAdminBoard(user.roles.includes("ROLE_ADMIN"));
    }

    EventBus.on("logout", logOut);

    return () => {
      EventBus.remove("logout", logOut);
    };
  }, []);

  const logOut = () => {
    AuthService.logout();
    setShowModeratorBoard(false);
    setShowAdminBoard(false);
    setCurrentUser(undefined);
  };

  return (
    <div>
      <nav className="navbar navbar-expand navbar-dark bg-dark">
        <Link to={"/"} className="navbar-brand">
          bezKoder
        </Link>
        <div className="navbar-nav mr-auto">
          <li className="nav-item">
            <Link to={"/home"} className="nav-link">
              Home
            </Link>
          </li>

          {showModeratorBoard && (
            <li className="nav-item">
              <Link to={"/mod"} className="nav-link">
                Moderator Board
              </Link>
            </li>
          )}

          {showAdminBoard && (
            <li className="nav-item">
              <Link to={"/admin"} className="nav-link">
                Admin Board
              </Link>
            </li>
          )}

          {currentUser && (
            <li className="nav-item">
              <Link to={"/user"} className="nav-link">
                User
              </Link>
            </li>
          )}
        </div>

        {currentUser ? (
          <div className="navbar-nav ml-auto">
            <li className="nav-item">
              <Link to={"/profile"} className="nav-link">
                {currentUser.username}
              </Link>
            </li>
            <li className="nav-item">
              <a href="/login" className="nav-link" onClick={logOut}>
                LogOut
              </a>
            </li>
          </div>
        ) : (
          <div className="navbar-nav ml-auto">
            <li className="nav-item">
              <Link to={"/login"} className="nav-link">
                Login
              </Link>
            </li>

            <li className="nav-item">
              <Link to={"/register"} className="nav-link">
                Sign Up
              </Link>
            </li>
          </div>
        )}
      </nav>

      <div className="container mt-3">
        <Switch>
          <Route exact path={["/", "/home"]} component={Home} />
          <Route exact path="/login" component={Login} />
          <Route exact path="/register" component={Register} />
          <Route exact path="/profile" component={Profile} />
          <Route path="/user" component={BoardUser} />
          <Route path="/mod" component={BoardModerator} />
          <Route path="/admin" component={BoardAdmin} />
        </Switch>
      </div>
    </div>
  );
};

export default App;

Logout when the Token is expired

There are two ways. For more details, please visit:
Handle JWT Token expiration in React with Hooks

Add CSS style for React Typescript Components

Open src/App.css and write some CSS code as following:

label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}

Configure Port for React Typescript Authentication with Web API

Because most of HTTP Server use CORS configuration that accepts resource sharing restricted to some sites or ports, so we also need to configure port for our App.

In project folder, create .env file with following content:

PORT=8081

Now we’ve set our app running at port 8081. You will need to do this work if you use one of following Servers:

Conclusion

Congratulation!

Today we’ve done so many interesting things. I hope you understand the overall layers of our React Typescript Authentication and Authorization Application (without Redux) using React Hooks, React Router, Axios, LocalStorage, Bootstrap. Now you can apply it in your project at ease.

You should continue to check if Token is expired and logout:
Handle JWT Token expiration in React with Hooks

Or add refresh token:
React Refresh Token with JWT and Axios Interceptors

If you want to use React Component for this example, you can find the implementation at:
React Typescript Login and Registration example

Or using Redux for state management:
React Redux Login, Logout, Registration example with Hooks

Happy learning, see you again!

Further Reading

Fullstack CRUD:
React + Spring Boot + MySQL
React + Spring Boot + PostgreSQL
React + Spring Boot + MongoDB
React + Node Express + MySQL
React + Node Express + PostgreSQL
React + Node Express + MongoDB
React + Django

Serverless with Firebase:
React Typescript CRUD example with Firebase Realtime Database
React Typescript CRUD example with Firebase Cloud Firestore

Source Code

You can find the complete source code for this example on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *