Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

adsofts's avatar

API for Application

Hello,

I would like to provide an API to centralize information used by different applications. I obviously found tons of tutorials on the net to learn how to implement Passport on a Laravel project. But this technique requires going through a User with a login and a password. However, these are applications that will consume my API, not users. So I would like each application to have a unique key (and possibly revocable) and that the authentication on the API is done only with this key to retrieve the connection token. This system seems relatively classic to me, but I can't find anything that explains how to do this with Laravel. I don't understand why a User would be needed to authenticate an application ... Anyone have a lead or even a good tutorial to do what I want? Thank you all Aurelian

0 likes
12 replies
rodrigo.pedra's avatar
Level 56

Take a look at Laravel Passport's Client Credentials Grant Tokens

https://laravel.com/docs/8.x/passport#client-credentials-grant-tokens

The client credentials grant is suitable for machine-to-machine authentication.

1 - Generating client tokens

You can issue different "client credentials grant" clients:

php artisan passport:client --client --name="App 1"
php artisan passport:client --client --name="App 2"
php artisan passport:client --client --name="App 3"

Then you would share with this external app's the columns id and secret generated for each call above in your oauth_clients table.

2 - Calling from external apps

An external application would first request an API token using those provided id and secret values

$guzzle = new GuzzleHttp\Client;

$response = $guzzle->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'client_credentials',
        'client_id' => 3, // column `id` on table `oauth_clients`
        'client_secret' => 'cioYH...', // column `secret` on table `oauth_clients`
        // 'scope' => 'your-scope', // optional
    ],
]);

$token = json_decode((string) $response->getBody(), true)['access_token'];

// save $token to use in calls to protected routes

You can store this token in an external app for reuse until it is expired.

Later these external apps would call protected routes using the token retrieved above:

$token = 'eyJ0eXAiOi...'; // saved token from above call

$guzzle = new GuzzleHttp\Client();

$response = $guzzle->get('http://your-app.com/api/protected', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
    ],
]);

$data = json_decode((string) $response->getBody(), true);

3 - Protecting routes to require a client token

Docs are pretty clear about it, but for completeness, you'd first add this route middleware to your ./app/Http/Kernel.php:

use Laravel\Passport\Http\Middleware\CheckClientCredentials;

protected $routeMiddleware = [
    'client' => CheckClientCredentials::class,
];

And use this 'client' middleware to protect routes from unauthorized access:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/protected', function () {
    return response()->json(['message' => 'Hello World']);
})->middleware('client');

4 - Revoking

Tokens issued are saved in oauth_access_tokens , and have expiration date (see Passport docs on how to change expiration time).

To revoke an access token just remove its corresponding record from the oauth_access_tokens table, or change its revoked column value to 1.

To remove an external app access altogether just remove its corresponding record from the oauth_clients table, or change its revoked column value to 1.

Hope it helps.

1 like
adsofts's avatar

Hello. By following your information as well as the official documentation, I was able to set up the API and everything works as I wanted. I just have a small problem, if I try to access a protected route without giving a valid token, I have an error: "Symfony \ Component \ Routing \ Exception \ RouteNotFoundException: Route [login] not defined" This route does not actually exist and I do not want it to exist. In this case I just want to return a json with a "token not supplied or invalid" error message. Do you know how I can do this? Again I can not find anything about it ... Thank you

rodrigo.pedra's avatar

Hi @adsofts ,

This is a Laravel convention I forgot to mention.

Let's look into Laravel\Passport\Http\Middleware\CheckClientCredentials code to see what it does when the request is unauthenticated:

protected function validateCredentials($token)
{
    if (! $token) {
        throw new AuthenticationException;
    }
}

https://github.com/laravel/passport/blob/5758a35582156ca0a116106389dd3d7f1055e1bf/src/Http/Middleware/CheckClientCredentials.php#L18-L23

So when no token is present it throws an AuthenticationException.

Then the base Exception Handler class handles an AuthenticationException like this:

protected function unauthenticated($request, AuthenticationException $exception)
{
    return $request->expectsJson()
                ? response()->json(['message' => $exception->getMessage()], 401)
                : redirect()->guest($exception->redirectTo() ?? route('login'));
}

https://github.com/laravel/framework/blob/c2d60b5ac186af29219549daf0806b4c9cdc4a21/src/Illuminate/Foundation/Exceptions/Handler.php#L384-L389

Problem is it checks for $request->expectsJson(). Let's look into the Request object (actually in the trait InteractsWithContentTypes used by it) to see what it returns in this method:

public function expectsJson()
{
    return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
}

https://github.com/laravel/framework/blob/c2d60b5ac186af29219549daf0806b4c9cdc4a21/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php#L42-L45

So we have a couple of checks:

  • $this->ajax() checks if the header X-Requested-With with the exact value XMLHttpRequest is present
  • $this->pjax() checks if the header X-PJAX is present
  • $this->acceptsAnyContentType() checks if header Accept is missing or it has one of these values: */*, or *
  • $this->wantsJson() checks if header Accept is missing or it value contains one o these strings: /json, or +json

As by the example I sent you no additional headers beyond the Authorization header are sent, $request->expectsJson() evaluates to false, and Laravel assumes the request is a regular browser request, thus trying to redirect to the login page.

One note: When comparing each constraint return value form the list above, remember to check against the $request->expectsJson() return boolean expression to see if the combined result evaluates to true.

Failing to meet this requirements is a very common as we often forget to add one of these headers.

Even Laravel acknowledges that in some way, by including this line in the default JavaScript scaffold:

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

https://github.com/laravel/laravel/blob/3adc2196f79fa4d8470d41d5a7584f2b0432a6fc/resources/js/bootstrap.js#L11

So it configures Axios (the most used HTTP client in JavaScript these days) to add the X-Requested-With: XMLHttpRequest header in every call to avoid unwanted redirects.

So what to do? Ask API consumers to meet additional requirements beyond sending the Authorization header? Doing this defeats the "Principle of least astonishment":

https://en.wikipedia.org/wiki/Principle_of_least_astonishment

What I do in my apps that expose an API is creating this middleware:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class EnsureJsonResponseForApi
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->is('api/*')) {
            $request->headers->set('Accept', 'application/json', true);
        }

        return $next($request);
    }
}

What it does is checking if the request's path starts with api/, and if so it sets the Accept header with a value of application/json. So later, if an exception is thrown, every check call to $request->expectsJson() returns true.

Note that the last parameter on the $request->headers->set(...) is true. The tells Laravel to replace any previous value if an Accept header is already present.

If your API can return content types different than json you can try setting the X-Requested-With header to XMLHttpRequest. Or just removing that parameter.

With this middleware available, I add it as the very first middleware to be run on the $middleware array in my ./app/Http/Kernel.php file:

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        // Add as the very first one, to the first middleware array, 
        // the global HTTP middleware stack.
        // not inside $middlewareGroups or $routeMiddleware
        \App\Http\Middleware\EnsureJsonResponseForApi::class,

        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Fruitcake\Cors\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    // ... other code
}

This will ensure JSON responses for every request in the /api path, even when the app is in maintenance mode.

If your app only exposes an API, and you want to prevent such redirects altogether, you could even skip the if check in the EnsureJsonResponseForApi and always set the Accept: application/json header.

Hope it makes sense. Forgot to mention about this issue before, sorry.

As an additional note:

This issue is not related on using Laravel Passport, Laravel Sanctum is also prone to it, or even using the built in Token Guard.

It is related on how Laravel handles some exceptions assuming a request is from a regular web app.

1 like
adsofts's avatar

you rock man ! thank's a lot

1 like
rodrigo.pedra's avatar

Hi @bezhansalleh , I didn't know of that, great to learn JetStream covers even machine-to-machine API calls.

As OP described:

However, these are applications that will consume my API, not users

Then I suggested using Passport Client Grants as requests wouldn't be bound to an specific user profile.

Can you point to me where in JetStream docs, where it describes how to implement API authentication guards that are not bound to users? in other words there should be no record in users table, no user profiles, no email/username and password login from the consumer app required for first token exchange? Or at least describe a general workflow on how to do it?

As OP wants two apps to communicate programmatically, without human interaction, no prior human navigation for the consumer app should be needed.

Also as OP described the central app as just a data provider API, I guess no screens would be needed, nor be available.

I took a brief look at JetStream docs and couldn't find any suitable information for programmatic machine-to-machine (or app-to-app) communication not bound to a user profile.

Thanks in advance, I didn't have the chance to try jetStream yet, so I am eager to learn about that =)

BezhanSalleh's avatar

Not based on your assumptions about the OP's intended use case, though OP sounds confused.

your way the interaction is through terminal and passport is heavy. I suggested jetstream which gives a gui and uses sanctum(kind of a starting point...) which is light.

Create a model named Applications extend Autheticatable, setup guard and bob is your uncle. but in all fairness it depends on the criteria, sanctum is best as an MVP and easy to swap to passport if required.

1 like
rodrigo.pedra's avatar

@bezhansalleh thanks for responding.

Indeed in the end of the day one needs to store the clients token somewhere, so leveraging the built-in feature of JetStream/Sanctum could come handy.

Also:

...and bob is your uncle

Made me laugh =) Thanks!

Have a nice day =)

1 like
adsofts's avatar

Hi guys,

@BezhanSalleh I don't know about JetStream. But indeed I share the point of view of @rodrigo.pedra insofar as my goal is to make applications communicate with the API independently of connected users. Concretely, this involves making technical vehicle information (unified repository) available for various sales or management applications. In any case, thank you for taking the time to answer me !

@rodrigo.pedra To begin with, thank you for your suggestion. Indeed it seems that this is exactly what I need. I'm not sure how I got past this by reading the Passport doc, but hey ... A big thank you for that! Then, I would like to thank you especially for the format of your answer. A lot of people would just say to me "look at Laravel Passport's Client Credentials Grant Tokens", you took the time to explain the different steps in detail to me and I find that very generous. So really, a big thank you to you

1 like
rodrigo.pedra's avatar

You're welcome!

Thanks for the remarks about the response style. I used this approach (client grant) before some times already in similar scenarios, so it was great to share.

Have a nice day =)

Please or to participate in this conversation.