rotaercz's avatar

Sanctum authentication help

So I have a new project which has the most recent version of Laravel and Jetstream installed.

I enabled the api-tokens page and made a test token. I'd like to use this token to login.

I was reading through the documentation here:

https://laravel.com/docs/8.x/sanctum#spa-authenticating

which says to use /sanctum/csrf-cookie which returns a XSRF-TOKEN and laravel_session (but no csrf-token). So I added the XSRF-TOKEN and laravel_session to the subsequent request header (this part is easy).

But don't I need a csrf-token to login?

In the documentation it says:

Sanctum allows you to issue API tokens / personal access tokens that may be used to authenticate API requests. When making requests using API tokens, the token should be included in the Authorization header as a Bearer token.

I tried sending the generated Sanctum token in the header as Authorization and also tried Bearer in the following format:

Authorization=sanctum_token;

Bearer=sanctum_token;

But I get a error 500.

Also tried sending as the csrf-token too.

What am I doing incorrectly and what am I missing?

0 likes
12 replies
rodrigo.pedra's avatar

So I added the XSRF-TOKEN and laravel_session to the subsequent request header....

I don't quite understand this. XSRF-TOKEN and laravel_session are cookies that once sent back when you visit /sanctum/csrf-cookie are going to be sent to the server on every request automatically, without the need to add them as headers.

But don't I need a csrf-token to login?

Yes, actually you need a CSRF token to post to any route. But as after visiting /sanctum/csrf-cookie the XSRF-TOKEN cookie is set and sent automatically with subsequent requests, Laravel can extract the CSRF token from that cookie.

Apologize me in advance if I am misunderstanding you, but I think you might be confusing the CSRF token with a user API token generated by Sanctum.

On guarded routes you will need to send an API token as an Authorization HTTP header with the request.

That header format is this:

Authorization: Bearer kifIJSWaXRKZfLEUlGdIjq3cAl9uREoPzTJiBLqx

Where kifIJSWaXRKZfLEUlGdIjq3cAl9uREoPzTJiBLqx would be the token Sanctum generated for a user. It is not the CSRF token or the XSRF-Token cookie value.

This API token should be sent as a HTTP header with your request to authorize guarded routes.

if you are using Postman to test your API check to see if you are adding it as a Header (header name: Authorization, header value: Bearer kifIJSWaX...) and not in the request's body.

If you are using Axios from JavaScript you can do this:

promise = axios.post('/my-route', {form: 'data'}, {
    headers: {Authorization: 'Bearer kifIJSWaX...'}
});

Very important, the Bearer part before the token must contain a single space between the word Bearer and the token value.

3 likes
rotaercz's avatar

Thank you for the detailed reply! It's really helpful! The header format for Authorization and Bearer was a part that I had not set correctly.

I should clarify that I'm trying to authenticate the user in Unity3D in C# for an online game. I'm constructing the web requests manually. Here's an example of how I'm doing it:

    readonly string loginURL = "http://website.com/login";

    WWWForm loginForm = new WWWForm();
    //loginForm.AddField( "_token", formToken ); // I don't have the formToken value
    loginForm.AddField( "email", email );
    loginForm.AddField( "password", pw );

    UnityWebRequest www = UnityWebRequest.Post( loginURL, loginForm );
    www.SetRequestHeader( "X-Requested-With", "XMLHttpRequest" );
    www.SetRequestHeader( "X-XSRF-TOKEN", xsrfToken );
    www.SetRequestHeader( "laravel_session", laravelSession );
    www.SetRequestHeader( "Authorization", authToken );

I currently visit /sanctum/csrf-cookie and I parse the response header for the XSRF-TOKEN and laravel_session data and fill in the xsrfToken and laravelSession variables respectively. I also set the authToken variable as you instructed (in the Bearer sanctum-api-token format).

I'm still getting a 500 error and it's because the formToken isn't available on form submission. Typically when a user visits the /login page before entering their credentials there's a csrf-token as a hidden field in the form similar to this:

    <input type="hidden" name="_token" id="csrf-token" value="{{ Session::token() }}" />

But since I'm directly trying to authenticate the user I don't have this value.

I'm currently trying to use Jetstream's /login route which is in web.php. Am I supposed to create a new /login route in api.php for Sanctum to authenticate the user?

(I can also emulate a web browser on the Unity Client side but it's more work. Fundamentally I'd like to authenticate the user in a secure manner. After secure authentication the TCP/UDP networking part of the code will take over. I'm open to ideas and suggestions.)

rodrigo.pedra's avatar
Level 56

Sanctum docs outline 2 authentication mechanisms:

  1. SPA Authentication
  2. API Token Authentication

SPA states for a Single Page Applications, which should be a JavaScript based application running in a browser, where aspects such as handling cookies are a built-in feature.

Your use-case fits better in the API Token Authentication scenario, as you are using another platform to interact with your app.

Sanctum docs has a section on "Mobile Application Authentication", that despite the name, outlines how an external app can obtain a token with a email/password and use it afterwards:

https://laravel.com/docs/8.x/sanctum#mobile-application-authentication

Based on that docs' part you could try the following:

Create a login route that returns an API token

First, create a controller:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class ApiLoginController
{
    use ValidatesRequests;

    public function __invoke(Request $request)
    {
        /////////////////////////////////
        // WILL ADD SOMETHING HERE LATER
        /////////////////////////////////

        $credentials = $this->validate($request, [
            'email' => 'required|string|email',
            'password' => 'required|string',
        ]);

        $user = User::query()->firstWhere('email', $credentials['email']);

        if (! $user) {
            return response()->json(['message' => 'invalid credentials'], 401);
        }

        if (! Hash::check($credentials['password'], $user->password)) {
            return response()->json(['message' => 'invalid credentials'], 401);
        }

        // User model should use the HasApiTokens trait,
        // as per Sanctum docs
        $token = $user->createToken('token-name');

        return response()->json(['token' => $token->plainTextToken], 200);
    }
}

Then register it in your ./routes/api.php file (IMPORTANT: not the web.php file)

use App\Http\Controllers\ApiLoginController;

Route::post('/login', ApiLoginController::class);

The catch here is that you now have an endpoint that accepts a pair of username and password and checks for a user login. Apparently no problem whatsoever, but as this endpoint has no additional security (note we are not using auth:api or auth:sanctum to protect it) and no CSRF protection, one could make a script that send requests to try brute forcing a user's password.

One way to solve this is to send a header with a token that identifies your C# application, so your server would know the request was issued by a first-party application.

Replace the comment block where there is this string WILL ADD SOMETHING HERE LATER with :

if ($request->header('X-APP-TOKEN') !== 'MY-SHARED-SECRET-TOKEN') {
    return response()->make('Forbidden.', 403);
}

ideally you would be reading this "shared secret" token from a config file.

This is not the best security measure, but it is valid one. It works a "shared secret" between both apps. As long you don't expose this token to anyone else it should improve this endpoint's security.

Then in your C# code you could do:

// Now your login endpoint is /api/login
readonly string loginURL = "http://website.com/api/login";

WWWForm loginForm = new WWWForm();
loginForm.AddField( "email", email );
loginForm.AddField( "password", pw );

UnityWebRequest www = UnityWebRequest.Post( loginURL, loginForm );

// You can keep this header, as Laravel 
// is happier with it when generating JSON error responses
www.SetRequestHeader( "X-Requested-With", "XMLHttpRequest" );

// Use the "shared secret" token used in ApiLoginController
www.SetRequestHeader( "X-APP-TOKEN", "MY-SHARED-SECRET-TOKEN" );

// process the response
// if successful save the returned token,
// otherwise handle any errors

One note: Compare this ApiLoginController to the implementation sample from Sanctum docs, to see which better fit your requirements.

Accessing guarded routes

With the token saved any subsequent calls to API routes guarded with auth:sanctum could be made a such:

readonly string loginURL = "http://website.com/api/another-endpoint";

WWWForm loginForm = new WWWForm();

UnityWebRequest www = UnityWebRequest.Get( loginURL );

// You can keep this header, as Laravel
// is happier with it when generating JSON error responses
www.SetRequestHeader( "X-Requested-With", "XMLHttpRequest" );

// Use the token returned in the login
www.SetRequestHeader( "Authorization", "Bearer " + userToken );

Logging out

Logging out is just a matter of deleting the token on your C# app.

Form time to time you might need to prune old tokens from your Laravel app.

Bottom line

As you can see we avoided the Cookies handling by leveraging Sanctum API token capabilities.

Bear in mind your API endpoints won't have a Session object. But that is intended as API endpoints are meant to be stateless.

Hope it makes things clearer and helps in some way.

3 likes
rotaercz's avatar

Form time to time you might need to prune old tokens from your Laravel app.

I'm thinking I could do this when the user logs out of the Unity app. Is it ok to just delete the corresponding row from the personal_access_tokens table in the database or is there a more correct way to do this? (Could you show me what a ApiLogoutController would look like that uses a guarded route?)

I can also see situations where the client crashes or something and the row doesn't get deleted. I suppose I could use a cron job that runs once a month in that scenario and it would delete the rows that are more than a month old.

Also, thanks again! The detailed answer you provided is really easy to follow and works well. :)

1 like
rodrigo.pedra's avatar

You're welcome!

Here there is a simple "logout" implementation:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\PersonalAccessToken;

class ApiLogoutController
{
    public function __invoke(Request $request)
    {
        $user = Auth::guard('sanctum')->user();

        if (! $user) {
            return \response()->noContent();
        }

        $accessToken = $user->currentAccessToken();

        if ($accessToken instanceof PersonalAccessToken) {
            $accessToken->delete();
        }

        return \response()->noContent();
    }
}

Note that I don't enforce a guard on the route to avoid errors if the request is unauthenticated as the token might be already been deleted before so the user won't be found.

But for it to work you would need to dispatch a request to this this API endpoint with the Authorization header just as you would to any other guarded route.

For the prune job you could create an artisan command, for simplicity I will show a closure-based implementation you can to your ./routes/console.php

<?php

use Illuminate\Support\Facades\Artisan;

Artisan::command('app:prune-sanctum', function () {
    // Delete tokens not used in a month
    \Laravel\Sanctum\PersonalAccessToken::query()
        ->where('last_used_at', '<', now()->subMonth())
        ->delete();
});

And add it to your console scheduler. More info on adding a command to your app's scheduler and configuring it with CRON can be found in the docs:

https://laravel.com/docs/8.x/scheduling

Hope all of these made the whole workflow clearer. Have a nice day =)

1 like
rodrigo.pedra's avatar

You're welcome!

See also my other response to your question down below. As Laracasts now allows to reply out of order you might have missed it.

Have a nice day =)

1 like
rotaercz's avatar

Amazing answer! I read through everything and have to go try this!

I do have a question regarding the 'MY-SHARED-SECRET-TOKEN' variable. Is this token something I would typically generate once on the server side similar to a 2-factor authentication key and then embed into the Unity Client? Or should it be constantly recreated similar to the XSRF-TOKEN using /sanctum/csrf-cookie?

rodrigo.pedra's avatar

Thanks.

This would be a once generated token and embedded in the client.

You could come up with a mechanism to update it once in a while, but that could lead to out-of-sync client applications.

The issue on using a mechanism similar to /sanctum/csrf-token is that by default Sanctum checks if the request comes from a first-party URL (or IP) it knows as valid. This acts similar to the "shared secret", as you have an allow-list of allowed URLs/IPs that can send that request.

In a C# application, I assume it will to be packaged and distributed to several customers running from different machines, and by consequence several IPs. So having an embedded "shared secret" token you can verify against provide an additional check.

As I said before it is not a perfect solution, but good enough to improve reliability and prevent unauthorized requests flooding your /api/login endpoint.

Some software that does activation embed a similar strategy, but in most cases they use a SSL key that is embedded into the software to encrypt this first request to identify a user.

That is a bit more hard to do, as it involves an additional layer of complexity (public and private keys, encrypting and decrypting requests on both client and server, etc.).

But unless your app needs need extra security, an embeded shared secret should be good enough.

1 like
rotaercz's avatar

Ah, I see. I was assuming each Unity client would have it's own MY-SHARED-SECRET-TOKEN variable and I would keep track on the server side in a database per user.

I'll go try setting this up!

rodrigo.pedra's avatar

Well packaging each Unity client with a different token require a lot of resources, imagine having to re-package your app just before a user try to download it.

The idea around this token is to add some sort of identification the client application dispatching the request is a known/authorized source.

Sort of a simplified client grant in a OAuth flow for machine-to-machine communication.

1 like
luckyfella73's avatar

Thanks for all help here! I'm building a laravel backend that get requests from a website running on a different subdomain that the laravel backend is running. If someone can clarify one thing for me: I read in the docs (and implemented that so far) that first you make an ajax call to the sanctum/csrf-cookie endpoint via axios for example and then you are able to make post requests while axois handles passing the needed information to laravel. What I don't understand is: the javascript making these calls runs in the browser, so how does laravel know if the website/js making the call was served by the domain specified as "whitelisted" in sanctum. php? Might be a stupid question but I would like to understand that :)

Please or to participate in this conversation.