Laravel 7 API Curd for Sanctum authentication

Sanctum is Laravel’s lightweight API authentication package. In this tutorial, I’ll be optically canvassing utilizing Sanctum to authenticate a React-predicated single-page app (SPA) with a Laravel backend. Surmising the front- and back-cessation of the app are sub-domains of the same top-level domain, we can utilize Sanctum’s cookie-predicated authentication, thereby preserving us the trouble of managing API tokens. To this end, I’ve set up Homestead to give me two domains: API.sanctum.test, which points to the public folder of a backend (the incipient Laravel project which we’ll engender), and sanctum.test, which points to a thoroughly separate directory, frontend. I’ve additionally provisioned a MySQL database, sanctum_backend.

Links to the final code can be found at the cessation of this article.

The backend

Let’s start with the API:

laravel new backend

Our API could be anything – let’s verbally express it’s for a library, and we have just one resource, books. We can engender most of what we require with one artisan command:

php artisan make:model Book -mr

The -m flag engenders a migration, while -r engenders a resourceful controller with methods for all the CRUD operations you will require. For this tutorial, we will only need index, but it’s good to ken this option subsists. So, let’s engender a couple of fields in the migration:

Schema::create('books', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('author');
    $table->timestamps();
});

…and run the migration (don’t forget to update the .env file with your database credentials):

php artisan migrate

Now update DatabaseSeeder.php to give us some books (and a utilizer for later):

Book::truncate();
$faker = \Faker\Factory::create();
for ($i = 0; $i < 50; $i++) {
    Book::create([
        'title' => $faker->sentence,
        'author' => $faker->name,
    ]);
}
User::truncate();
User::create([
    'name' => 'Alex',
    'email' => 'alex@alex.com',
    'password' => Hash::make('pwdpwd'),
]);

Now run php artisan db:seed to seed this data. Determinately, we require to engender the route and the controller action. That’s simple enough. Integrate this to the routes/api.php file:

Route::get('/book', 'BookController@index');

and then in the index method of BookController, return all the books:

return response()->json(Book::all());

Of course in an authentic API, we would probably want to transform those objects utilizing something like Laravel’s API resources, but this will do for now. Now if we hit API.sanctum.test/API/book in our browser or HTTP client of a cull (Postman, Insomnia, etc), you should visually perceive a list of all the books.

The frontend

To create the SPA, I’ll use create-react-app:

npx create-react-app frontend
cd frontend

We’re going to operate to utilize the react-router-dom package to integrate routing to the app, as well as Axios to make HTTP requests. Once that’s done, start the app:

npm install axios react-router-dom
npm start

Now let’s engender an expeditious Books component that will utilize Axios to call the books endpoint and show the books in an unordered list:

import React from 'react';
import axios from 'axios';

const Books = () => {
    const [books, setBooks] = React.useState([]);
    React.useEffect(() => {
        axios.get('https://api.sanctum.test/api/book')
        .then(response => {
            setBooks(response.data)
        })
        .catch(error => console.error(error));
    }, []);
    const bookList = books.map((book) => 
        <li key={book.id}>{book.title}</li>
    );
    return (
        <ul>{bookList}</ul>
    );
}

export default Books;

Reference this component in App.js and we’re good to go:

import React from 'react';
import { BrowserRouter as Router, Switch, Route, NavLink } from 'react-router-dom';
import Books from './components/Books';

const App = () => {
    return (
        <Router>
            <div>
                <NavLink to='/books'>Books</NavLink>
            </div>
            <Switch>
                <Route path='/books' component={Books} />
            </Switch>
        </Router>
    );
};

export default App;

Visit the books page in the browser, and you’ll visually perceive the list of books returned by the endpoint.

Now, let’s verbalize we optate to gate-keep who gets to visually examine these books. Or maybe the API exhibits different books to different users. This is where Sanctum comes into play. So, back in the backend directory, let’s require the Sanctum package:

composer require laravel/sanctum laravel/ui

We’re withal requiring the laravel/ui package because it gives us some authentication boilerplate. To engender it, and to publish the Sanctum config, run:

php artisan ui:auth
php artisan vendor:publish

…and add the sanctum middleware to the route:

Route::middleware('auth:sanctum')->get('/book', 'BookController@index');

Since our goal is to have the frontend – sanctum.test – communicating with the backend – api.sanctum.test – it makes sense from now on to build our SPA with npm run build. This way we can visit the SPA at sanctum.test (rather than the development server’s default localhost). This will make more sense when we come to configuring Sanctum’s stateful domains.

So, build the frontend, and endeavor to hit that books page again. If you optically canvass the request in your browser’s dev implements, you should optically discern a 401 Unauthenticated error: we require to authenticate. Here’s the SPA’s Authenticate component:

import React from 'react';
import axios from 'axios';

const Login = (props) => {
    const [email, setEmail] = React.useState('');
    const [password, setPassword] = React.useState('');
    const handleSubmit = (e) => {
        e.preventDefault();
        axios.post('https://api.sanctum.test/login', {
            email: email,
            password: password
        }).then(response => {
            console.log(response)
        });
    }
    return (
        <div>
            <h1>Login</h1>
            <form onSubmit={handleSubmit}>
                <input
                    type="email"
                    name="email"
                    placeholder="Email"
                    value={email}
                    onChange={e => setEmail(e.target.value)}
                    required
                />
                <input
                    type="password"
                    name="password"
                    placeholder="Password"
                    value={password}
                    onChange={e => setPassword(e.target.value)}
                    required
                />
                <button type="submit">Login</button>  
            </form>
        </div>
    );
}

export default Login;

It’s just a fundamental form, that utilizes Axios to post an electronic mail and password to the backend’s authenticate route and log the replication. Integrate it to the App component:

import Login from './components/Login';
[...]
<Switch>
    <Route path='/books' component={Books} />
    <Route path='/login' component={Login} />
</Switch>

Now, visit the authenticate page, fill out the user’s details (seeded above), and hit “Login”. Oops! Take a visual examination of the console: it’s giving us a “Cross-Inchoation Request Blocked” error.

A digression on CORS

The same-inception policy is a security measure embedded in browsers, which obviates scripts running on one inception (where an inchoation is defined by its scheme [http, https, ftp, etc], hostname and port number) from accessing data stored on another inception. In our context, this has particular applicability to Fetch / XMLHttpRequest calls. The same-inchoation policy, while it sanctions us to make calls to other domains (hence opening us up to CSRF attacks, for which visually perceive later), does not sanction us to read replications from other domains.

CORS (Cross-Inchoation Resource Sharing) is a browser solution to this issue: it sanctions you to send an Inchoation header with your request, while the server’s replication has an Access-Control-Sanction-Inchoation header. If the two match, then the replication is approved and can be received by the browser.

All well and good, but if you look in the network tab, you will optically discern that we don’t even get as far as making a POST request. In fact, all we visually perceive is an OPTIONS request. Why is that? Well, the reason is that our request doesn’t qualify as a soi-disant “simple request”, because its Content-Type header is application/json. This makes it a “preflighted request”: afore the authentic request is sent, a “preflight” OPTIONS request is sent to the server, which will respond with a set of headers from which the browser can determine whether to proceed to make the authentic request. Since our Laravel app isn’t yet set up for CORS, it doesn’t send any Access-Control- headers back, and so the request felicitous doesn’t take place. The front-end side is genuinely covered for us, because the browser automatically sends the Inception header with the request. So we just need to establish the backend.

Authentically, as of Laravel 7 the framework comes with a CORS middleware out of the box. It’s configurable in the cors config file. Open it up, and you’ll visually perceive that sanctioned_origins is by default set to * – that is, everything can make read requests. So why isn’t it working? Well, a little further up there’s a paths key, which sanctions anything in the api namespace. But our authenticate route by default is in the root namespace: /authenticate. So let’s integrate ‘authenticate’ to the paths array. Now fill out the authenticate form again and submit it.

CSRF

An incipient error! 419. Check the replication: “CSRF token mismatch”. On to our next issue! CSRF stands for “Cross-Site Request Forgery”: it’s a way for a malignant agent to execute actions in an authenticated environment. An example, from the OWASP guide: You are authenticated in to your online banking website. Via convivial engineering, you are illuded into visiting a website while you are still authenticated in to the bank’s site. This “visit” to the hacker’s URL could be obnubilated in a 0x0 image in an electronic mail, or an enticing link, or whatever. Anyway, this URL will hit the bank’s API and do something awful with your account. Because you’re already authenticated in to the bank, it won’t require going through any authentication steps. Horror ensues.

How to bypass this? One way, the way we will pursue here, is to get the server to send an arbitrary token in a cookie to the client, which then includes the token as a custom header with every request to the server. If we run php artisan route:list, we’ll visually perceive that the authenticate route belongs to the web middleware group, which includes the VerifyCsrfToken middleware. In its handle method we visually perceive this condition:

if (
    $this->isReading($request) ||
    $this->runningUnitTests() ||
    $this->inExceptArray($request) ||
    $this->tokensMatch($request)
)

If this condition evaluates to erroneous, a TokenMismatchException is thrown. Now, since we are not reading (we’re sending a POST request), and not running unit tests, and there is nothing configured as an exception, it will run the tokensMatch method. And since we haven’t sent a token, this will withal fail, and so we get the exception.

Ok, so this establishes the gate-keeping for us. But how do we get the CSRF token in the first place? If we were staying on the server side, we could get Laravel to pass it within the framework, from a controller to a view, for example. But our view is not accommodated by the framework, so somehow the framework needs to send the CSRF token to us. The api auth sentinel won’t do that for us out of the box. This is where Sanctum comes in. Sanctum will sanction us to ask for a CSRF token, which we can then pass in our headers. If you run

php artisan route:list

you’ll optically discern an incipient route there: GET /sanctum/csrf-cookie. (How does the framework ken about this? It emanates from the defineRoutes method, which is in the SanctumServiceProvider‘s boot method, which in turn was triggered when we ran artisan vendor:publish.)

So let’s make our first call to the CSRF route. Back in the authenticate code for the frontend, I’m going to modify the Axios call that’s made when you submit the authenticate form. First it will make a call to request the CSRF token; then it will make the authenticate call:

axios.get('https://api.sanctum.test/sanctum/csrf-cookie')
    .then(response => {
        axios.post('https://api.sanctum.test/login', {
            email: email,
            password: password
        }).then(response => {
            console.log(response)
        })
    });

Fill in the form, hit return, and… Error! “Cross-Inception Request Blocked”. I mentally conceived we’d dealt with this? We had: but now we require to integrate this incipient route to our list of paths in the cors config file:

'paths' => ['api/*', 'login', 'sanctum/csrf-cookie'],

Now, afore you hit submit the form again, open the browser’s dev implements and look in the “Storage” tab (Firefox) or the “Application” tab (Chrome). Hopefully, you’ll visually perceive no cookies in there (if you do, efface them). Now submit the form. And… still no cookies! What’s going erroneous here?

Take a look in the Network tab: your call to sanctum/csrf-cookie is getting a 204 replication, which is good. Click on the request and then click on the Cookies tab: you’ll optically discern two cookies, the Laravel session cookie and the one we optate, XSRF-TOKEN. But if you go to the browser storage these cookies aren’t being preserved. Why not?

Well, this has to do with the scope of the cookie. As the MDN document suggests, the Domain directive will sanction you to designate subdomains to which the cookie is applicable. Let’s take a visual examination of the anatomy of the XSRF-TOKEN cookie, which is visible in the replication headers to the network request:

XSRF-TOKEN=<token>; expires=Sat, 02-May-2020 21:40:15 GMT; Max-Age=7200; path=/; samesite=lax

Sure enough, there is no domain directive there. Let’s add it in our backend’s .env file:

SESSION_DOMAIN=sanctum.test

Now, resubmit the authenticate request, and the cookies are still not listed… This is because we’re missing one more piece of the puzzle in our frontend. If we optically canvass the MDN docs, we visually perceive the following:

XMLHttpRequest responses from a different domain cannot set cookie values for their own domain unless withCredentials is set to true before making the request

So we require to set withCredentials to true in our Axios configuration. Since we’re going to require to do that for all requests, let’s refactor the SPA code to centralize the API configuration. Engender an incipient folder accommodations in the src directory, and integrate a file api.js with the following contents:

import axios from 'axios';

const apiClient = axios.create({
    baseURL: 'https://api.sanctum.test',
    withCredentials: true,
});

export default apiClient;

Now we can import that in our Book and Login components:

import apiClient from '../services/api';

And in lieu of calling axios, we call apiClient, omitting the hostname since we’ve defined that in the baseURL of our Axios config:

apiClient.get('/sanctum/csrf-cookie')
    .then(response => {
        apiClient.post('/login', {
            email: email,
            password: password
        }).then(response => {
            console.log(response)
        })
    });

Now, authenticate again, and visually examine the browser implements for the cookies: this time they should appear. But if you check the console, you’ll visually perceive that “Cross-Inchoation Request Blocked” error again, but this time with an incipient reason: “expected ‘true’ in CORS header ‘Access-Control-Sanction-Credentials’”. To be extra safe, the browser will only perform this request if the server has this flag set to true. We can do this by setting fortifies_credentials to true in cors.php.

Now if you authenticate with the correct credentials (the ones you seeded earlier on), you will visually perceive a 204 replication from the authenticate request. This is good: you are authenticated. But if you head to the “Books” page, you’ll still get the 401 Unauthenticated error when Axios calls the book endpoint. The way to fine-tune this is with Sanctum’s “stateful domains”. Open up app/Http/Kernel.php, and integrate the EnsureFrontendRequestsAreStateful middleware to the api group:

'api' => [
    EnsureFrontendRequestsAreStateful::class,
    'throttle:60,1',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

Let’s take a look at the handle method of this class to see what it does:

config([
    'session.http_only' => true,
    'session.same_site' => 'lax',
]);

First, it overrides the session config. It sets http_only to true, denoting that a client-side script (for example a maleficent script that is utilizing XSS to endeavor to assail your app) has no access to the token. (As this OWASP article verbally expresses, “the majority of XSS attacks target larceny of session cookies”.) It withal sets same_site to “lax”. According to the MDN docs, this will avert cookies being sent for cross-site requests except for when the request emanates from a link to your site from another site (this blog post does a good job of expounding why that’s utilizable).

return (new Pipeline(app()))
    ->send($request)
    ->through(static::fromFrontend($request) ? [
        // Middleware
    ] : [])
    ->then(function ($request) use ($next) {
        return $next($request);
    });

To understand this component of the method, it’s auxiliary to ken that Laravel’s middleware is processed by a Pipeline, which is a Laravel utility class that sanctions you to concatenate an array of pipes to send data through. If you optically canvass Illuminate\Substratum\Http\Kernel.php‘s sendRequestThroughRouter method, you’ll visually perceive homogeneous code to the above:

return (new Pipeline($this->app))
    ->send($request)
    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
    ->then($this->dispatchToRouter());

So what Sanctum’s EnsureFrontendRequestsAreStateful middleware does is genuinely insert more middleware. But only if the request is emanating from the frontend – that’s the purport of this check:

static::fromFrontend($request) ? [ 
    // some middleware 
] : []

If the request is emanating from the frontend, queue up this middleware, otherwise, just give the pipeline a vacuous array. The static method fromFrontend visually examines the referer header: if it contains the string you’ve set in the Sanctum config, it will ken the request should be put through the middleware concrete to Sanctum. This string that it compares the referer header with can be set with the SANCTUM_STATEFUL_DOMAINS variable in .env:

SANCTUM_STATEFUL_DOMAINS=sanctum.test

And what is the Sanctum-specific middleware?

[
    config('sanctum.middleware.encrypt_cookies', \Illuminate\Cookie\Middleware\EncryptCookies::class),
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    \Illuminate\Session\Middleware\StartSession::class,
    config('sanctum.middleware.verify_csrf_token', \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class),
]

These four middleware pipes are standard: if you take a look at the web middleware in Kernel.php, you’ll see all four of them there.

EncryptCookies: Encrypting a cookie betokens that even if an assailant can gain access to the cookie, modifying its content will result in the cookie being abnegated by the server when it is sent back.
AddQueuedCookiesToResponse: Handles any cookies that have been queued with the Cookie facade.
StartSession: Establishes the Laravel session along with its session cookie, which it integrates to the replication.
VerifyCsrfToken: Checks that everything’s in order with the CSRF token.

Authentication

Now, integrating this middleware sorts out the cookie process. But the authentic authentication occurs because we’ve set auth:sanctum in our API route. This denotes: utilize the “sanctum” sentinel to authenticate. But if we optically canvass the Sanctum sentinel class, something seems aberrant. According to the docs for integrating custom sentinels, a custom sentinel has to implement the Illuminate\Contracts\Auth\Sentinel interface. But none of this interface’s methods are included in this sentinel, which in any case has no implements keyword in the class definition. Instead, we just have this __invoke magic method.

Let’s optically canvass the SanctumServiceProvider to elucidate this. It utilizes the $auth->elongate method as exhorted in the docs:

$auth->extend('sanctum', function ($app, $name, array $config) use ($auth) {
    return tap($this->createGuard($auth, $config), function ($guard) {
        $this->app->refresh('request', $guard, 'setRequest');
    });
});

This is discombobulating, so let’s break it down into more diminutive components. The tap command is a short-hand way of saying “create the sentinel, then pass it to the closure in the second argument; then return the guard”. Let’s visually examine createGuard:

return new RequestGuard(
    new Guard($auth, config('sanctum.expiration'), $config['provider']),
    $this->app['request'],
    $auth->createUserProvider()
);

First of all, we can visually perceive that this returns an instance of RequestGuard, which, since it implements Sentinel, satiates the elongate method’s argument type. This RequestGuard takes a closure as its first argument, which in our case is Sanctum’s Sentinel class. The only distinction is that the “closure” (Sanctum’s Sentinel class) is a class with an __invoke magic method: you can cerebrate of this kind of class as a closure-with-state: it gives you a simple invokable function which withal can have properties.

RequestGuard then uses the callback to return a user. Here are the relevant lines in the Sanctum guard:

if ($user = $this->auth->guard(config('sanctum.guard', 'web'))->user()) {
    return $this->supportsTokens($user)
                ? $user->withAccessToken(new TransientToken)
                : $user;
}

The first line gets the utilizer from the web sentinel (since we are utilizing the customary web authentication routes to authenticate). If a utilizer is found, the sentinel returns it; otherwise, nothing is returned.

And now, once you’ve set the SANCTUM_STATEFUL_DOMAINS environment variable, you should be able to authenticate and view the books page as an authenticated utilizer.

Finishing the SPA

So, now we have a working authentication system on the backend, we can culminate off the front-end. This component of the article isn’t explicitly cognate to Sanctum, so feel in liberty to ignore it.

First, we want some state in the App component to show whether the user has logged in, defaulting to false:

const [loggedIn, setLoggedIn] = React.useState(false);

Let’s add a method called login which sets this variable to true:

const login = () => {
    setLoggedIn(true);
};

Now we can pass this method to the Login component:

<Route path='/login' render={props => (
    <Login {...props} login={login} />
)} />

then in our handleSubmit method call this authenticate method, after checking that we have got the expected 204 replication from calling the authenticate route:

apiClient.get('/sanctum/csrf-cookie')
    .then(response => {
        apiClient.post('/login', {
            email: email,
            password: password
        }).then(response => {
            if (response.status === 204) {
                props.login();
            }
        })
    });

(I additionally have some logic to redirect to the homepage after logging in – take an optical canvassing of the final repo to visually perceive that.) Now that the parent App component kens when a utilizer is authenticated in, it can pass this to the Books component so that it can act accordingly:

<Route path='/books' render={props => (
    <Books {...props} loggedIn={loggedIn} />
)} />

Now, if loggedIn is erroneous, the Books component kens not to endeavor to load the books and instead to show the utilizer a subsidiary message:

React.useEffect(() => {
    if (props.loggedIn) {
        apiClient.get('/api/book')
        .then(response => {
            setBooks(response.data)
        })
        .catch(error => console.error(error));
    }
});
// ...
if (props.loggedIn) {
    return (
        <ul>{bookList}</ul>
    );
}
return (
    <div>You are not logged in.</div>
);

How about logging out? Let’s modify the current Authenticate link to conditionally exhibit a Logout button if the utilizer is authenticated in, and a link to the Authenticate page if they’re not authenticated in.

const authLink = loggedIn 
    ? <button onClick={logout}>Logout</button>
    : <NavLink to='/login'>Login</NavLink>;
return (
    <Router>
        <div>
            <NavLink to='/books'>Books</NavLink>
            {authLink}
        </div>
        <Switch>
            <Route path='/books' component={Books} />
            <Route path='/login' render={props => (
                <Login {...props} login={login} />
            )} />
        </Switch>
    </Router>
);

Now we can integrate a logout method. Laravel’s auth scaffolding provides us with a POST route to logout, so we can integrate a method to the App component:

const logout = () => {
    apiClient.post('/logout').then(response => {
        if (response.status === 204) {
            setLoggedIn(false);
        }
    })
};    

Try this, and… oops! Another CORS error. Add logout to the paths array in our cors.php config file:

'paths' => ['api/*', 'login', 'logout', 'sanctum/csrf-cookie'],

Now authenticate, log out again and you should optically discern the menu item updating. And after you’ve logged out, you won’t be able to access the books page.

The final step is to preserve the loggedIn boolean to the browser’s storage. If we don’t do this, when the utilizer refreshes the browser, the SPA will reset the utilizer to being not logged-in. We can utilize the browser’s sessionStorage API for that:

const [loggedIn, setLoggedIn] = React.useState(
    sessionStorage.getItem('loggedIn') == 'true' || false
);
const login = () => {
    setLoggedIn(true);
    sessionStorage.setItem('loggedIn', true);
};
const logout = () => {
    apiClient.post('/logout').then(response => {
        if (response.status === 204) {
            setLoggedIn(false);
            sessionStorage.setItem('loggedIn', false);
        }
    })
}; 

The final code for the backend and frontend can be found here:

credit to : https://laravel-news.com/using-sanctum-to-authenticate-a-react-spa

About OakML

Hi we are enthusiastic developers team, we try to serve some good idea to open source community

Leave a Reply

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

Subscribe To Our Newsletter
Enter your email to receive a weekly round-up of our best posts. Learn more!