├── .env
├── src
├── home
│ ├── index.js
│ └── Home.jsx
├── login
│ ├── index.js
│ └── Login.jsx
├── index.css
├── _components
│ ├── index.js
│ ├── PrivateRoute.jsx
│ └── Nav.jsx
├── _helpers
│ ├── index.js
│ ├── history.js
│ ├── fetch-wrapper.js
│ └── fake-backend.js
├── _store
│ ├── index.js
│ ├── users.slice.js
│ └── auth.slice.js
├── index.js
└── App.jsx
├── public
├── robots.txt
├── favicon.ico
└── index.html
├── jsconfig.json
├── README.md
├── .gitignore
├── package.json
└── LICENSE
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=http://localhost:4000
--------------------------------------------------------------------------------
/src/home/index.js:
--------------------------------------------------------------------------------
1 | export * from './Home';
--------------------------------------------------------------------------------
/src/login/index.js:
--------------------------------------------------------------------------------
1 | export * from './Login';
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .app-container {
2 | min-height: 350px;
3 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/_components/index.js:
--------------------------------------------------------------------------------
1 | export * from './Nav';
2 | export * from './PrivateRoute';
3 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": ["src"]
6 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cornflourblue/react-18-redux-jwt-authentication-example/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/_helpers/index.js:
--------------------------------------------------------------------------------
1 | export * from './fake-backend';
2 | export * from './fetch-wrapper';
3 | export * from './history';
4 |
--------------------------------------------------------------------------------
/src/_helpers/history.js:
--------------------------------------------------------------------------------
1 | // custom history object to allow navigation outside react components
2 | export const history = {
3 | navigate: null,
4 | location: null
5 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-18-redux-jwt-authentication-example
2 |
3 | React 18 + Redux - JWT Authentication Example & Tutorial
4 |
5 | Documentation at https://jasonwatmore.com/post/2022/06/15/react-18-redux-jwt-authentication-example-tutorial
6 |
7 | Documentación en español en https://jasonwatmore.es/post/2022/06/15/react-18-redux-ejemplo-y-tutorial-de-autenticacion-jwt
8 |
--------------------------------------------------------------------------------
/src/_store/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 |
3 | import { authReducer } from './auth.slice';
4 | import { usersReducer } from './users.slice';
5 |
6 | export * from './auth.slice';
7 | export * from './users.slice';
8 |
9 | export const store = configureStore({
10 | reducer: {
11 | auth: authReducer,
12 | users: usersReducer
13 | },
14 | });
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/_components/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import { useSelector } from 'react-redux';
3 |
4 | import { history } from '_helpers';
5 |
6 | export { PrivateRoute };
7 |
8 | function PrivateRoute({ children }) {
9 | const { user: authUser } = useSelector(x => x.auth);
10 |
11 | if (!authUser) {
12 | // not logged in so redirect to login page with the return url
13 | return
14 | }
15 |
16 | // authorized so return child components
17 | return children;
18 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import { Provider } from 'react-redux';
4 | import { BrowserRouter } from 'react-router-dom';
5 |
6 | import { store } from './_store';
7 | import { App } from './App';
8 | import './index.css';
9 |
10 | // setup fake backend
11 | import { fakeBackend } from './_helpers';
12 | fakeBackend();
13 |
14 | const container = document.getElementById('root');
15 | const root = createRoot(container);
16 |
17 | root.render(
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 |
--------------------------------------------------------------------------------
/src/_components/Nav.jsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from 'react-router-dom';
2 | import { useSelector, useDispatch } from 'react-redux';
3 |
4 | import { authActions } from '_store';
5 |
6 | export { Nav };
7 |
8 | function Nav() {
9 | const authUser = useSelector(x => x.auth.user);
10 | const dispatch = useDispatch();
11 | const logout = () => dispatch(authActions.logout());
12 |
13 | // only show nav when logged in
14 | if (!authUser) return null;
15 |
16 | return (
17 |
18 |
19 | Home
20 | Logout
21 |
22 |
23 | );
24 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React 18 + Redux - JWT Authentication Example
8 |
9 |
10 |
11 |
12 |
13 | You need to enable JavaScript to run this app.
14 |
15 |
16 |
17 |
25 |
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-18-redux-jwt-authentication-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@hookform/resolvers": "^2.9.0",
7 | "@reduxjs/toolkit": "^1.8.2",
8 | "react": "^18.1.0",
9 | "react-dom": "^18.1.0",
10 | "react-hook-form": "^7.31.3",
11 | "react-redux": "^8.0.2",
12 | "react-router-dom": "^6.3.0",
13 | "react-scripts": "5.0.1",
14 | "yup": "^0.32.11"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": [
23 | "react-app"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Jason Watmore
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
2 |
3 | import { history } from '_helpers';
4 | import { Nav, PrivateRoute } from '_components';
5 | import { Home } from 'home';
6 | import { Login } from 'login';
7 |
8 | export { App };
9 |
10 | function App() {
11 | // init custom history object to allow navigation from
12 | // anywhere in the react app (inside or outside components)
13 | history.navigate = useNavigate();
14 | history.location = useLocation();
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 | }
28 | />
29 | } />
30 | } />
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 |
4 | import { userActions } from '_store';
5 |
6 | export { Home };
7 |
8 | function Home() {
9 | const dispatch = useDispatch();
10 | const { user: authUser } = useSelector(x => x.auth);
11 | const { users } = useSelector(x => x.users);
12 |
13 | useEffect(() => {
14 | dispatch(userActions.getAll());
15 |
16 | // eslint-disable-next-line react-hooks/exhaustive-deps
17 | }, []);
18 |
19 | return (
20 |
21 |
Hi {authUser?.firstName}!
22 |
You're logged in with React 18 + Redux & JWT!!
23 |
Users from secure api end point:
24 | {users.length &&
25 |
26 | {users.map(user =>
27 | {user.firstName} {user.lastName}
28 | )}
29 |
30 | }
31 | {users.loading &&
}
32 | {users.error &&
Error loading users: {users.error.message}
}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/_store/users.slice.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
2 |
3 | import { fetchWrapper } from '_helpers';
4 |
5 | // create slice
6 |
7 | const name = 'users';
8 | const initialState = createInitialState();
9 | const extraActions = createExtraActions();
10 | const extraReducers = createExtraReducers();
11 | const slice = createSlice({ name, initialState, extraReducers });
12 |
13 | // exports
14 |
15 | export const userActions = { ...slice.actions, ...extraActions };
16 | export const usersReducer = slice.reducer;
17 |
18 | // implementation
19 |
20 | function createInitialState() {
21 | return {
22 | users: {}
23 | }
24 | }
25 |
26 | function createExtraActions() {
27 | const baseUrl = `${process.env.REACT_APP_API_URL}/users`;
28 |
29 | return {
30 | getAll: getAll()
31 | };
32 |
33 | function getAll() {
34 | return createAsyncThunk(
35 | `${name}/getAll`,
36 | async () => await fetchWrapper.get(baseUrl)
37 | );
38 | }
39 | }
40 |
41 | function createExtraReducers() {
42 | return {
43 | ...getAll()
44 | };
45 |
46 | function getAll() {
47 | var { pending, fulfilled, rejected } = extraActions.getAll;
48 | return {
49 | [pending]: (state) => {
50 | state.users = { loading: true };
51 | },
52 | [fulfilled]: (state, action) => {
53 | state.users = action.payload;
54 | },
55 | [rejected]: (state, action) => {
56 | state.users = { error: action.error };
57 | }
58 | };
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/_helpers/fetch-wrapper.js:
--------------------------------------------------------------------------------
1 | import { store, authActions } from '_store';
2 |
3 | export const fetchWrapper = {
4 | get: request('GET'),
5 | post: request('POST'),
6 | put: request('PUT'),
7 | delete: request('DELETE')
8 | };
9 |
10 | function request(method) {
11 | return (url, body) => {
12 | const requestOptions = {
13 | method,
14 | headers: authHeader(url)
15 | };
16 | if (body) {
17 | requestOptions.headers['Content-Type'] = 'application/json';
18 | requestOptions.body = JSON.stringify(body);
19 | }
20 | return fetch(url, requestOptions).then(handleResponse);
21 | }
22 | }
23 |
24 | // helper functions
25 |
26 | function authHeader(url) {
27 | // return auth header with jwt if user is logged in and request is to the api url
28 | const token = authToken();
29 | const isLoggedIn = !!token;
30 | const isApiUrl = url.startsWith(process.env.REACT_APP_API_URL);
31 | if (isLoggedIn && isApiUrl) {
32 | return { Authorization: `Bearer ${token}` };
33 | } else {
34 | return {};
35 | }
36 | }
37 |
38 | function authToken() {
39 | return store.getState().auth.user?.token;
40 | }
41 |
42 | function handleResponse(response) {
43 | return response.text().then(text => {
44 | const data = text && JSON.parse(text);
45 |
46 | if (!response.ok) {
47 | if ([401, 403].includes(response.status) && authToken()) {
48 | // auto logout if 401 Unauthorized or 403 Forbidden response returned from api
49 | const logout = () => store.dispatch(authActions.logout());
50 | logout();
51 | }
52 |
53 | const error = (data && data.message) || response.statusText;
54 | return Promise.reject(error);
55 | }
56 |
57 | return data;
58 | });
59 | }
--------------------------------------------------------------------------------
/src/_store/auth.slice.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
2 |
3 | import { history, fetchWrapper } from '_helpers';
4 |
5 | // create slice
6 |
7 | const name = 'auth';
8 | const initialState = createInitialState();
9 | const reducers = createReducers();
10 | const extraActions = createExtraActions();
11 | const extraReducers = createExtraReducers();
12 | const slice = createSlice({ name, initialState, reducers, extraReducers });
13 |
14 | // exports
15 |
16 | export const authActions = { ...slice.actions, ...extraActions };
17 | export const authReducer = slice.reducer;
18 |
19 | // implementation
20 |
21 | function createInitialState() {
22 | return {
23 | // initialize state from local storage to enable user to stay logged in
24 | user: JSON.parse(localStorage.getItem('user')),
25 | error: null
26 | }
27 | }
28 |
29 | function createReducers() {
30 | return {
31 | logout
32 | };
33 |
34 | function logout(state) {
35 | state.user = null;
36 | localStorage.removeItem('user');
37 | history.navigate('/login');
38 | }
39 | }
40 |
41 | function createExtraActions() {
42 | const baseUrl = `${process.env.REACT_APP_API_URL}/users`;
43 |
44 | return {
45 | login: login()
46 | };
47 |
48 | function login() {
49 | return createAsyncThunk(
50 | `${name}/login`,
51 | async ({ username, password }) => await fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
52 | );
53 | }
54 | }
55 |
56 | function createExtraReducers() {
57 | return {
58 | ...login()
59 | };
60 |
61 | function login() {
62 | var { pending, fulfilled, rejected } = extraActions.login;
63 | return {
64 | [pending]: (state) => {
65 | state.error = null;
66 | },
67 | [fulfilled]: (state, action) => {
68 | const user = action.payload;
69 |
70 | // store user details and jwt token in local storage to keep user logged in between page refreshes
71 | localStorage.setItem('user', JSON.stringify(user));
72 | state.user = user;
73 |
74 | // get return url from location state or default to home page
75 | const { from } = history.location.state || { from: { pathname: '/' } };
76 | history.navigate(from);
77 | },
78 | [rejected]: (state, action) => {
79 | state.error = action.error;
80 | }
81 | };
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/_helpers/fake-backend.js:
--------------------------------------------------------------------------------
1 | export { fakeBackend };
2 |
3 | function fakeBackend() {
4 | let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }];
5 | let realFetch = window.fetch;
6 | window.fetch = function (url, opts) {
7 | return new Promise((resolve, reject) => {
8 | // wrap in timeout to simulate server api call
9 | setTimeout(handleRoute, 500);
10 |
11 | function handleRoute() {
12 | switch (true) {
13 | case url.endsWith('/users/authenticate') && opts.method === 'POST':
14 | return authenticate();
15 | case url.endsWith('/users') && opts.method === 'GET':
16 | return getUsers();
17 | default:
18 | // pass through any requests not handled above
19 | return realFetch(url, opts)
20 | .then(response => resolve(response))
21 | .catch(error => reject(error));
22 | }
23 | }
24 |
25 | // route functions
26 |
27 | function authenticate() {
28 | const { username, password } = body();
29 | const user = users.find(x => x.username === username && x.password === password);
30 |
31 | if (!user) return error('Username or password is incorrect');
32 |
33 | return ok({
34 | id: user.id,
35 | username: user.username,
36 | firstName: user.firstName,
37 | lastName: user.lastName,
38 | token: 'fake-jwt-token'
39 | });
40 | }
41 |
42 | function getUsers() {
43 | if (!isAuthenticated()) return unauthorized();
44 | return ok(users);
45 | }
46 |
47 | // helper functions
48 |
49 | function ok(body) {
50 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) })
51 | }
52 |
53 | function unauthorized() {
54 | resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorized' })) })
55 | }
56 |
57 | function error(message) {
58 | resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) })
59 | }
60 |
61 | function isAuthenticated() {
62 | return opts.headers['Authorization'] === 'Bearer fake-jwt-token';
63 | }
64 |
65 | function body() {
66 | return opts.body && JSON.parse(opts.body);
67 | }
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/login/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useForm } from "react-hook-form";
3 | import { yupResolver } from '@hookform/resolvers/yup';
4 | import * as Yup from 'yup';
5 | import { useSelector, useDispatch } from 'react-redux';
6 |
7 | import { history } from '_helpers';
8 | import { authActions } from '_store';
9 |
10 | export { Login };
11 |
12 | function Login() {
13 | const dispatch = useDispatch();
14 | const authUser = useSelector(x => x.auth.user);
15 | const authError = useSelector(x => x.auth.error);
16 |
17 | useEffect(() => {
18 | // redirect to home if already logged in
19 | if (authUser) history.navigate('/');
20 |
21 | // eslint-disable-next-line react-hooks/exhaustive-deps
22 | }, []);
23 |
24 | // form validation rules
25 | const validationSchema = Yup.object().shape({
26 | username: Yup.string().required('Username is required'),
27 | password: Yup.string().required('Password is required')
28 | });
29 | const formOptions = { resolver: yupResolver(validationSchema) };
30 |
31 | // get functions to build form with useForm() hook
32 | const { register, handleSubmit, formState } = useForm(formOptions);
33 | const { errors, isSubmitting } = formState;
34 |
35 | function onSubmit({ username, password }) {
36 | return dispatch(authActions.login({ username, password }));
37 | }
38 |
39 | return (
40 |
41 |
42 | Username: test
43 | Password: test
44 |
45 |
69 |
70 | )
71 | }
72 |
--------------------------------------------------------------------------------