Backend DevelopmentFrontend Development

Laravel Sanctum Authentication In React Apps

Laravel Sanctum Authentication In React Apps

In this tutorial we will implement laravel authentication functionality using Sanctum in a react app shipped in the same laravel installation.

 

 

 

We talked about Laravel 8 authentication using Sanctum in a previous tutorial using Vuejs, you can check it here to get a better idea of how Sanctum works.

However as a quick description of Sanctum, the laravel sanctum package provides authentication system for (single page applications), mobile applications, and simple, token based APIs. You can think of sanctum as a replacement of the JWT packages you may have used in previous version of laravel.

The advantage of using Sanctum also is that it provides both the normal session cookie based authentication and token authentication. If your spa in the same repository as the laravel application then sanctum will use the cookie based authentication. If the your spa or mobile app is on a different repository or domain then sanctum will try to use the token authentication.

In our tutorial here we will create a simple react app inside of the same laravel application.

So let’s begin by creating a fresh laravel application.

composer create-project laravel/laravel:8.x.x lara_react_auth --prefer-dist

Next we need to setup the database. Create a new mysql database using myql CLI or phpmyadmin, name it whatever you want and update the .env file with database credentials:

.env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=<db name>
DB_USERNAME=<db user>
DB_PASSWORD=<db password>

 

Preparing Sanctum

In default laravel 8 installation Sanctum already shipped with laravel, as in my case so no other configuration is needed, just check your composer.json file to see if laravel/sanctum exist.

If you don’t have Sanctum package installed just follow the below steps:

composer require laravel/sanctum

Next, publish the Sanctum configuration and migration files using the vendor:publish Artisan command

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

After running this command a new configuration file sanctum.php generated in the config/ directory.

Next, make sure that the Sanctum middleware in the api middleware group in Kernel.php

app/Http/Kernel.php

'api' => [
             \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,     // sanctum middleware
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

This middleware is responsible for ensuring that incoming requests from your react app can authenticate using Laravel’s session cookies.

Finally run the migrations to generate the user and sanctum related tables:

php artisan migrate

Now if you look at the db you will see a new table personal_access_tokens. This table related to Sanctum and store the access tokens in case you used the api tokens instead of cookies for authentication.

 

Sanctum Config

You can customize the configuration of Sanctum in config/sanctum.php. An important configuration option you have to care about is the “stateful” key:

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
        Sanctum::currentApplicationUrlWithPort()
    ))),

This option allows to configure which domains your SPA will be making requests from. So this option used in case you use Laravel session cookies when making requests to your API.

In local development the above setting should work, but there are cases when you need to set these values correctly according to your frontend domain app. In my case i will leave these values as is because we are using react and laravel in the same project and running on port 8000.

 

Installing React

In order to use react scaffolding in Laravel 8 we have to install laravel/ui package.

So let’s install it

composer require laravel/ui

Next generate React scaffolding:

php artisan ui react

Then install npm dependencies:

npm install

Install react router:

npm install react-router-dom@6

The react router enable us to navigate through different react pages.

Run laravel mix to generate the compiled assets app.css and app.js

npm run dev

The next step is to create a new view file that will contain our react app.

resources/views/home.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel React Auth</title>

    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">

    <style>
        body {
            font-family: 'Nunito', sans-serif;
        }
        
        .invalid-feedback {
           display: block !important;
        }
    </style>

    <link href="{{mix('css/app.css')}}" type="text/css" rel="stylesheet" />

</head>
<body class="antialiased">

    <div id="app"></div>

    <script src="{{mix('js/app.js')}}" type="text/javascript"></script>
</body>
</html>

Then update the routes file routes/web.php

<?php

use Illuminate\Support\Facades\Route;

Route::get('/{path?}', function () {
    return view('home');
})->where('path', '^(?!api).*?');

This route as you would expect load our react app in the home view, and here i am using where expression to tell laravel router to look for any route except routes that start with “/api”.

 

Preparing React Pages

We will need three pages for our app:

  • Home.js
  • Login.js
  • Register.js

Besides these pages we need two other components:

  • Layout.js (for main layout for all pages)
  • Navbar.js (to hold the navigation links)

In the context of react we will create three components and we will create a navbar component that contains the navigation links to move to each page.

So go to resources/js/components and create a new directory pages/ and inside the pages/ directory create the above files as follows:

resources/js/components/pages/Home.js

import React from "react";

export const Home = () => {
    return (
            <div className="row justify-content-center">
                <div className="col-md-8">
                    <div className="card">
                        <div className="card-header">Laravel React Auth</div>

                        <div className="card-body">Home Page</div>
                    </div>
                </div>
            </div>
    );
};

resources/js/components/pages/Login.js

export const Login = (props) => {
    return (
        <div className="row justify-content-center">
            <div className="col-md-8">
                <div className="card">
                    <div className="card-header">Login</div>

                    <div className="card-body">
                        <form method="POST" action="#">

                            <div className="row mb-3">
                                <label htmlFor="email" className="col-md-4 col-form-label text-md-end">Email Address</label>

                                <div className="col-md-6">
                                    <input id="email" type="email"
                                           className="form-control" name="email"
                                            required autoComplete="email" autoFocus />
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label htmlFor="password" className="col-md-4 col-form-label text-md-end">Password</label>

                                <div className="col-md-6">
                                    <input id="password" type="password"
                                           className="form-control" name="password"
                                           required autoComplete="current-password" />
                                </div>
                            </div>

                            <div className="row mb-3">
                                <div className="col-md-6 offset-md-4">
                                    <div className="form-check">
                                        <input className="form-check-input" type="checkbox" name="remember"
                                               id="remember" checked />

                                            <label className="form-check-label" htmlFor="remember">
                                                Remember Me
                                            </label>
                                    </div>
                                </div>
                            </div>

                            <div className="row mb-0">
                                <div className="col-md-8 offset-md-4">
                                    <button type="submit" className="btn btn-primary">
                                        Login
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    );
};

resources/js/components/pages/Register.js

export const Register = (props) => {
    return (
        <div className="row justify-content-center">
            <div className="col-md-8">
                <div className="card">
                    <div className="card-header">Register</div>

                    <div className="card-body">
                        <form method="POST" action="#">

                            <div className="row mb-3">
                                <label htmlFor="name" className="col-md-4 col-form-label text-md-end">Name</label>

                                <div className="col-md-6">
                                    <input id="name" type="text"
                                           className="form-control" name="name" required autoComplete="name" autoFocus />
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label htmlFor="email" className="col-md-4 col-form-label text-md-end">E-Mail Address</label>

                                <div className="col-md-6">
                                    <input id="email" type="email"
                                           className="form-control" name="email" required autoComplete="email" />
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label htmlFor="password" className="col-md-4 col-form-label text-md-end">Password</label>

                                <div className="col-md-6">
                                    <input id="password" type="password"
                                           className="form-control"
                                           name="password" required autoComplete="new-password" />
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label htmlFor="password-confirm" className="col-md-4 col-form-label text-md-end">Confirm Password</label>

                                <div className="col-md-6">
                                    <input id="password-confirm" type="password" className="form-control"
                                           name="password_confirmation" required autoComplete="new-password" />
                                </div>
                            </div>

                            <div className="row mb-0">
                                <div className="col-md-6 offset-md-4">
                                    <button type="submit" className="btn btn-primary">
                                        Register
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    );
};

Also create a layout file resources/js/components/Layout.js

import React from "react";
import Navbar from "./Navbar";

export const Layout = ({ children }) => {
    return (
            <div>
                <Navbar />
                <div className="container">
                    { children }
                </div>
            </div>
    );
};

Create resources/js/components/Navbar.js

import React from "react";
import {Link} from "react-router-dom";

function Navbar() {
    return (
        <nav className="navbar navbar-expand-md navbar-light bg-white shadow-sm">
            <div className="container">
                <Link to="/" className="navbar-brand">Laravel React Auth</Link>
                <button className="navbar-toggler" type="button" data-bs-toggle="collapse"
                        data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span className="navbar-toggler-icon"></span>
                </button>

                <div className="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul className="navbar-nav me-auto">
                    </ul>

                    <ul className="navbar-nav ms-auto">
                        <li className="nav-item">
                            <Link className="nav-link" to="/login">Login</Link>
                        </li>

                        <li className="nav-item">
                            <Link className="nav-link" to="/register">Register</Link>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    );
};

export default Navbar;

Finally create a new root component inside components/

resources/js/components/RootApp.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Routes, Route } from "react-router-dom";

import {Layout} from "./Layout";
import {Home} from "./pages/Home";
import {Login} from "./pages/Login";
import {Register} from "./pages/Register";

function RootApp() {
    return (
        <Layout>
            <Routes>
                <Route path="/" element={<Home /> } />
                <Route path="/login" element={<Login /> } />
                <Route path="/register" element={<Register /> } />
            </Routes>
        </Layout>
    );
}

export default RootApp;

if (document.getElementById('app')) {
    ReactDOM.render(
        <BrowserRouter>
            <RootApp />
        </BrowserRouter>
            , document.getElementById('app'));
}

Then update the app.js to include the new root component

resources/js/app.js

require('./bootstrap');

require('./components/RootApp');

The next step open the terminal and run:

npm run dev

Then run to launch the project:

php artisan serve

If you go now to http://localhost:8000 you will see the react app and you can click to any link to move to any page.

 

Login & Register Controllers

Let’s add the code to register and login users in backend. I already created two controllers and populated the code to login, and register users.

Create a new directory inside of app/Http/Controllers named Api with the below controllers:

app/Http/Controllers/Api/Register.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class RegisterController extends Controller
{

    /**
     * create new user
     *
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:4', 'confirmed'],
        ]);

        if($validator->fails()) {
            return response()->json(['status' => false, 'message' => 'fix errors', 'errors' => $validator->errors()], 500);
        }

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        return response()->json(['status' => true, 'user' => $user]);
    }
}

app/Http/Controllers/Api/Login.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class LoginController extends Controller
{

    public function login(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required|string',
            'password' => 'required|string',
        ]);

        if($validator->fails()) {
            return response()->json(['status' => false, 'message' => 'fix errors', 'errors' => $validator->errors()], 500);
        }

        $credentials = $request->only('email', 'password');

        if(auth()->attempt($credentials, $request->filled('remember'))) {
            return response()->json(['status' => true, 'user' => auth()->user()]);
        }

        return response()->json(['status' => false, 'message' => 'invalid username or password'], 500);
    }

    public function logout(Request $request)
    {
        auth('web')->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return response()->json(['status' => true, 'message' => 'logged out']);
    }

    public function me()
    {
        return response()->json(['status' => true, 'user' => auth()->user()]);
    }
}

Next add the associated routes to these controller actions.

Open routes/web.php and add these routes:

Route::group(['prefix' => 'api'], function () {
    Route::post('/login', [\App\Http\Controllers\Api\LoginController::class, 'login']);
    Route::post('/register', [\App\Http\Controllers\Api\RegisterController::class, 'register']);

    Route::group(['middleware' => 'auth:sanctum'], function () {
        Route::post('/logout', [App\Http\Controllers\Api\LoginController::class, 'logout']);
        Route::post('/me', [App\Http\Controllers\Api\LoginController::class, 'me']);
    });
});

As you see in this code i wrapped the two routes for /logout and /me with the ‘auth:sanctum‘ middleware instead of the normal laravel auth middleware. This is important to ensure that all the incoming requests are authenticated as either a stateful authenticated requests from your SPA or contain a valid API token header.

The next thing we have to do in the react components, let’s call the above apis in the designated pages using axios.

resources/js/components/pages/Register.js

import React, {useState} from "react";
import {useForm} from "../../hooks/useForm";

const Register = (props) => {

    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [passwordConfirmation, setPasswordConfirmation] = useState('');

    const { setErrors, renderFieldError, navigate } = useForm();

    const makeRequest = (e) => {
        e.preventDefault();

        setErrors(null);

        axios.post('/api/register', {
            name,
            email,
            password,
            password_confirmation: passwordConfirmation
        }).then(response => {

            console.log(response.data.user);

            if(response.data.user) {
                alert("Register success");

                navigate('/login');
            }
        }).catch(error => {
            console.log(error);

            if(error.response) {
                if (error.response.data.errors) {
                    setErrors(error.response.data.errors);
                }
            }
        });
    };

    return (
        <div className="row justify-content-center">
            <div className="col-md-8">
                <div className="card">
                    <div className="card-header">Register</div>

                    <div className="card-body">
                        <form method="POST" action="#" onSubmit={makeRequest}>

                            <div className="row mb-3">
                                <label className="col-md-4 col-form-label text-md-end">Name</label>

                                <div className="col-md-6">
                                    <input id="name" type="text"
                                           className="form-control" name="name" required autoComplete="name" autoFocus value={name} onChange={e => setName(e.target.value)} />
                                    {renderFieldError('name')}
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label className="col-md-4 col-form-label text-md-end">E-Mail Address</label>

                                <div className="col-md-6">
                                    <input id="email" type="email"
                                           className="form-control" name="email" required autoComplete="email" value={email} onChange={e => setEmail(e.target.value)} />
                                    {renderFieldError('email')}
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label className="col-md-4 col-form-label text-md-end">Password</label>

                                <div className="col-md-6">
                                    <input id="password" type="password"
                                           className="form-control"
                                           name="password" required autoComplete="new-password" value={password} onChange={e => setPassword(e.target.value)} />
                                    {renderFieldError('password')}
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label className="col-md-4 col-form-label text-md-end">Confirm Password</label>

                                <div className="col-md-6">
                                    <input id="password-confirm" type="password" className="form-control"
                                           name="password_confirmation" required autoComplete="new-password" value={passwordConfirmation} onChange={e => setPasswordConfirmation(e.target.value)} />
                                    {renderFieldError('password_confirmation')}
                                </div>
                            </div>

                            <div className="row mb-0">
                                <div className="col-md-6 offset-md-4">
                                    <button type="submit" className="btn btn-primary">
                                        Register
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    );
};

export {Register};

resources/js/components/pages/Login.js

import React, { useState } from "react";
import {useForm} from "../../hooks/useForm";

export const Login = (props) => {

    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [remember, setRemember] = useState(false);

    const { setErrors, renderFieldError, message, setMessage, navigate } = useForm();

    const makeRequest = (e) => {
        e.preventDefault();

        setErrors(null);

        setMessage('');

        // make request first to sanctum/csrf-cookie
        axios.get('/sanctum/csrf-cookie').then(() => {

           const payload = {
              email,
              password
           };

            if(remember) {
                payload.remember = true;
            }
            axios.post('/api/login', payload, {headers: { 'Accept': 'application/json' } }).then(response => {

                console.log(response.data.user);

                if(response.data.user) {
                    alert('Login success');
                    navigate('/');
                }
            }).catch(error => {
                console.log(error);

                if(error.response) {
                    if (error.response.data.message) {
                        setMessage(error.response.data.message);
                    }

                    if (error.response.data.errors) {
                        setErrors(error.response.data.errors);
                    }
                }
            });
        });
    };

    return (
        <div className="row justify-content-center">
            <div className="col-md-8">
                <div className="card">
                    <div className="card-header">Login</div>

                    <div className="card-body">

                        {
                            message && <div className="alert alert-danger">{message}</div>
                        }

                        <form method="POST" action="#" onSubmit={makeRequest}>

                            <div className="row mb-3">
                                <label htmlFor="email" className="col-md-4 col-form-label text-md-end">Email Address</label>

                                <div className="col-md-6">
                                    <input id="email" type="email"
                                           className="form-control" name="email"
                                            required autoComplete="email" autoFocus value={email} onChange={e => setEmail(e.target.value)} />
                                    {renderFieldError('email')}
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label htmlFor="password" className="col-md-4 col-form-label text-md-end">Password</label>

                                <div className="col-md-6">
                                    <input id="password" type="password"
                                           className="form-control" name="password"
                                           required autoComplete="current-password" value={password} onChange={e => setPassword(e.target.value)} />
                                    { renderFieldError('password') }
                                </div>
                            </div>

                            <div className="row mb-3">
                                <div className="col-md-6 offset-md-4">
                                    <div className="form-check">
                                        <input className="form-check-input" type="checkbox" name="remember"
                                               id="remember" onChange={e => { setRemember(e.target.checked ? 1 : 0) } } />

                                            <label className="form-check-label" htmlFor="remember">
                                                Remember Me
                                            </label>
                                    </div>
                                </div>
                            </div>

                            <div className="row mb-0">
                                <div className="col-md-8 offset-md-4">
                                    <button type="submit" className="btn btn-primary">
                                        Login
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    );
};

resources/js/hooks/useForm.js

import React, { useState } from "react";
import {useNavigate} from "react-router-dom";

export const useForm = () => {
    let navigate = useNavigate();

    const [errors, setErrors] = useState(null);
    const [message, setMessage] = useState('');

    function renderFieldError(field) {
        if(errors && errors.hasOwnProperty(field)) {
            return errors[field][0] ? (
                <span className="invalid-feedback" role="alert"><strong>{errors[field][0]}</strong></span>
            ) : null;
        }

        return null;
    }

    return {
        navigate,
        errors,
        setErrors,
        message,
        setMessage,
        renderFieldError
    }
}

At first the i created the useForm hook which is a helper hook to use it in both the Login and Register forms. It contains some state info like errors and message and a single function renderFieldError() to use it to display validation errors below specific form field.

Then in the Register and Login components all i have done is setting some state variables like name, email, password which be sent along with the axios request.

In the Register component when submitting the form we make an axios request using the makeRequest() function and sending the form data. Upon success we return the user back to the login page. In case of failure we set the errors state variable using setErrors() to be displayed in the form.

the Login component works the same way, but the most unique thing here is that before making the post request to /api/login you have to make a get request to /sanctum/csrf-cookie endpoint which to initialize CSRF protection for the application.

During this request, Laravel will set an XSRF-TOKEN cookie containing the current CSRF token. This token should then be passed in an X-XSRF-TOKEN header on subsequent requests.

After the user login successfully it will be redirected to the home page and the browser headers will contain the XSRF-TOKEN cookie.

To test this run laravel mix:

npm run dev

Now go the register page and create a single user, then login and open your browser dev tools you shoud see the XSRF-TOKEN cookie.

laravel XSRF Token cookie

The axios is smart enough when detecting an XSRF-TOKEN in browser cookies so it will send it along all subsequent requests.

To confirm this let’s open Home.js and make a request to /api/me endpoint. This endpoint requires that you should be authenticated.

resources/js/components/pages/Home.js

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

export const Home = () => {

    const [user, setUser] = useState(null);

    useEffect(() => {

        axios.post('/api/me').then(response => {
            setUser(response.data.user);
        });
    }, []);

    return (
            <div className="row justify-content-center">
                <div className="col-md-8">
                    <div className="card">
                        <div className="card-header">Laravel React Auth</div>

                        <div className="card-body">
                            {
                                user && (
                                    <>
                                        <p>Signed in</p>
                                        <div>Hi {user.name}</div>
                                    </>
                                )
                            }
                        </div>
                    </div>
                </div>
            </div>
    );
};

Run laravel mix again:

npm run dev

Then login again, now when redirected to the home page you should see “Hi your name”.

If you encountered any troubles regarding this endpoint or get a response of “unauthenticated” just double check the stateful option in config/sanctum.php and set it with the right domain.

 

Persisting User Auth Status

At this point we have finished authenticating the user but there are a couple of issues need to be addressed:

  • We need to store the authentication status after the user is logged in some kind of storage like cookies.
  • The navbar links should be updated to reflect the login status of the user.
  • If i refresh the page we have to make a request to the backend to check if i still logged in or not and in turn updating the login status.

To address all these issues i have made two things, a context and a helper hook.

resources/js/context/authContext.js

import React from "react";

const authData = {
  signedIn: false,
  user: null
};

export default React.createContext({authData: {...authData}, setAuthData: (val) => {}});

resources/js/hooks/useAuth.js

import React, {useContext, useEffect} from "react";
import {Cookies} from "react-cookie";
import {useNavigate} from "react-router-dom";

import AuthContext from "../context/authContext";

export const useAuth = () => {
    let navigate = useNavigate();

    const [userData, setUserdata] = React.useState({signedIn: false, user: null});

    const {setAuthData} = useContext(AuthContext);

    useEffect(() => {
        setAuthData(userData);
    }, [userData.signedIn]);

    function getAuthCookieExpiration()
    {
        let date = new Date();
        date.setTime(date.getTime() + (7 * 24 * 60 * 60 * 1000));  // 7 days
        return date;
    }

    function setAsLogged(user) {

        const cookie = new Cookies();

        cookie.set('is_auth', true, {path: '/', expires: getAuthCookieExpiration(), sameSite: 'lax', httpOnly: false});

        setUserdata({signedIn: true, user});

        navigate('/');
    }

    function setLogout() {
        const cookie = new Cookies();

        cookie.remove('is_auth', {path: '/', expires: getAuthCookieExpiration(), sameSite: 'lax', httpOnly: false});

        setUserdata({signedIn: false, user: null});

        navigate('/login');
    }

    function loginUserOnStartup()
    {
        const cookie = new Cookies();
        if(cookie.get('is_auth')) {

            axios.post('/api/me').then(response => {
                setUserdata({signedIn: true, user: response.data.user});
                navigate('/');
            }).catch(error => {
                setUserdata({signedIn: false, user: null});
                setLogout();
            });

        } else {
            setUserdata({signedIn: false, user: null});
            navigate('/login');
        }
    }

    return {
        userData,
        setAsLogged,
        setLogout,
        loginUserOnStartup
    }

};

The AuthContext is the provider or central store that supply the authentication state through the app. In this case the provider contains an object of the two keys (signedIn) and (user).

The useAuth hook contains some initialization state and some helper functions that we use when performing login or logout and thereby updating the state which triggers the context to be updated.

Now let’s apply this context and the custom hook in our components.

resources/js/components/RootApp.js

....

import {useAuth} from "../hooks/useAuth";
import AuthContext from "../context/authContext";

.....

function RootApp() {

    const {userData} = useAuth();
    const [authData, setAuthData] = useState({signedIn: userData.signedIn, user: userData.user});

    return (
        <AuthContext.Provider value={{authData, setAuthData }}>
            <Layout>
                <Routes>
                    <Route path="/" element={<Home /> } />
                    <Route path="/login" element={<Login /> } />
                    <Route path="/register" element={<Register /> } />
                </Routes>
            </Layout>
        </AuthContext.Provider>
    );
}

Also update resources/js/components/pages/Home.js

import AuthContext from "../../context/authContext";

export const Home = () => {

    const {authData} = useContext(AuthContext);
    const navigate = useNavigate();

    useEffect(() => {
        if(!authData.signedIn) {
            navigate('/login');
        }
    }, []);

    return (
            <div className="row justify-content-center">
                <div className="col-md-8">
                    <div className="card">
                        <div className="card-header">Laravel React Auth</div>

                        <div className="card-body">
                            {
                                authData.signedIn && authData.user && (
                                    <>
                                        <p>Signed in</p>
                                        <div>Hi {authData.user.name}</div>
                                    </>
                                )
                            }
                        </div>
                    </div>
                </div>
            </div>
    );
};

resources/js/components/Pages/Login.js

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

import {useForm} from "../../hooks/useForm";
import {useAuth} from "../../hooks/useAuth";
import AuthContext from "../../context/authContext";

export const Login = (props) => {

    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [remember, setRemember] = useState(false);

    const {setAsLogged} = useAuth();

    const {authData} = useContext(AuthContext);

    useEffect(() => {
        if(!authData.signedIn) {
            navigate('/');
        }
    }, []);

    const { setErrors, renderFieldError, message, setMessage, navigate } = useForm();

    const makeRequest = (e) => {
        e.preventDefault();

        setErrors(null);

        setMessage('');

        // make request first to sanctum/csrf-cookie
        axios.get('/sanctum/csrf-cookie').then(() => {

            const payload = {
                email,
                password
            };

            if(remember) {
                payload.remember = true;
            }

            axios.post('/api/login', payload, {
                headers: {
                    'Accept': 'application/json'
                }
            }).then(response => {

                if(response.data.user) {

                    alert("Login success");

                    setAsLogged(response.data.user);
                }
            }).catch(error => {
                console.log(error);

                if(error.response) {
                    if (error.response.data.message) {
                        setMessage(error.response.data.message);
                    }

                    if (error.response.data.errors) {
                        setErrors(error.response.data.errors);
                    }
                }
            });
        });
    };

    return (
        <div className="row justify-content-center">
            <div className="col-md-8">
                <div className="card">
                    <div className="card-header">Login</div>

                    <div className="card-body">

                        {
                            message && <div className="alert alert-danger">{message}</div>
                        }

                        <form method="POST" action="#" onSubmit={makeRequest}>

                            <div className="row mb-3">
                                <label htmlFor="email" className="col-md-4 col-form-label text-md-end">Email Address</label>

                                <div className="col-md-6">
                                    <input id="email" type="email"
                                           className="form-control" name="email"
                                            required autoComplete="email" autoFocus value={email} onChange={e => setEmail(e.target.value)} />
                                    {renderFieldError('email')}
                                </div>
                            </div>

                            <div className="row mb-3">
                                <label htmlFor="password" className="col-md-4 col-form-label text-md-end">Password</label>

                                <div className="col-md-6">
                                    <input id="password" type="password"
                                           className="form-control" name="password"
                                           required autoComplete="current-password" value={password} onChange={e => setPassword(e.target.value)} />
                                    { renderFieldError('password') }
                                </div>
                            </div>

                            <div className="row mb-3">
                                <div className="col-md-6 offset-md-4">
                                    <div className="form-check">
                                        <input className="form-check-input" type="checkbox" name="remember"
                                               id="remember" onChange={e => { setRemember(e.target.checked ? 1 : 0) } } />

                                            <label className="form-check-label" htmlFor="remember">
                                                Remember Me
                                            </label>
                                    </div>
                                </div>
                            </div>

                            <div className="row mb-0">
                                <div className="col-md-8 offset-md-4">
                                    <button type="submit" className="btn btn-primary">
                                        Login
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    );
};

resources/js/components/Navbar.js

.....

import AuthContext from "../context/authContext";
import {useAuth} from "../hooks/useAuth";

function Navbar() {

    const {authData} = useContext(AuthContext);
    const {setLogout} = useAuth();

    const renderLinks = () => {
        if(!authData.signedIn) {
            return (
                <>
                    <li className="nav-item">
                        <Link className="nav-link" to="/login">Login</Link>
                    </li>

                    <li className="nav-item">
                        <Link className="nav-link" to="/register">Register</Link>
                    </li>
                </>
            )
        }

        return (
            <>
                <li className="nav-item">
                    <a className="nav-link" href="">Hi {authData.user.name}</a>
                </li>
                <li className="nav-item">
                 <a className="nav-link" href="#" onClick={handleLogout}>Logout</a>
                </li>
            </>
        )
    }

    const handleLogout = () => {
        axios.post('/api/logout').then(response => {
            setLogout();
        }).catch(err => {
            console.log(err);
        })
    }

    return (
        <nav className="navbar navbar-expand-md navbar-light bg-white shadow-sm">
            <div className="container">
                <Link to="/" className="navbar-brand">Laravel React Auth</Link>
                <button className="navbar-toggler" type="button" data-bs-toggle="collapse"
                        data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span className="navbar-toggler-icon"></span>
                </button>

                <div className="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul className="navbar-nav me-auto">
                    </ul>

                    <ul className="navbar-nav ms-auto">
                        { renderLinks() }
                    </ul>
                </div>
            </div>
        </nav>
    );
};

export default Navbar;

resources/js/components/Layout.js

.....

import {useAuth} from "../hooks/useAuth";

export const Layout = ({ children }) => {

    const { loginUserOnStartup } = useAuth();

    useEffect(() => {
        loginUserOnStartup();
    }, []);

    return (
            <div>
                <Navbar />
                <div className="container">
                    { children }
                </div>
            </div>
    );
};

run laravel mix:

npm run dev

Now login again and refresh the page you see that app is already authenticated and redirects you to the homepage. Also if you inspect the browser dev tools you will see the is_auth cookie is set. Once this cookie is expired you will be logged out.

If you notice there is a flashing happens when refreshing the page appear on a fraction of second, and this happens because the signedIn key is initially false and it takes some time to be updated in the useEffect() hook.

To overcome this problem you can use a more robust solution like router guard, you can read more about it in this link.

 

4.5 8 votes
Article Rating

What's your reaction?

Excited
14
Happy
6
Not Sure
4
Confused
6

You may also like

Subscribe
Notify of
guest

9 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Shady
Shady
2 years ago

Very good job, it was a big help for me. Thx !!

Kamal Kunwar
2 years ago

Hello, I followed your article and setup on my local but I got error on login.
I got following error You should call navigate() in a React.useEffect(), not when your component is first rendered.
One thing I changed, I used laravel 9 and react 18.

Kamal Kunwar
2 years ago

Hello can you please update this on github please? I got error.
You should call navigate() in a React.useEffect(), not when your component is first rendered. and could not login.

Blade
Blade
2 years ago

Hello what is the solution for this errors please ?
You should call navigate() in a React.useEffect(), not when your component is first rendered

Blade
Blade
2 years ago
Reply to  WebMobTuts

I dont have this error anymore but when i logged in i have to refresh the page to display my name, do you know why ?

Juan
Juan
1 year ago

Thank you very much, your guide helped me to set the sanctum authentication for my ReactJS app