19
votes

I'm writing a tiny sms gateway to be consumed by a couple of projects,

I implemented laravel passport authentication (client credentials grant token)

Then I've added CheckClientCredentials to api middleware group:

protected $middlewareGroups = [
    'web' => [
       ...
    ],

    'api' => [
        'throttle:60,1',
        'bindings',
        \Laravel\Passport\Http\Middleware\CheckClientCredentials::class
    ],
];

The logic is working fine, now in my controller I need to get client associated with a valid token.

routes.php

Route::post('/sms', function(Request $request) {
    // save the sms along with the client id and send it

    $client_id = ''; // get the client id somehow

    sendSms($request->text, $request->to, $client_id);
});

For obvious security reasons I can never send the client id with the consumer request e.g. $client_id = $request->client_id;.

9
Isn't Auth::user()->token()->client->id sufficient in your case?thijsai
@thijsai there is no user in client credentials grant token, it is used for machine-to-machine authentication, each request i'm sending client id & secret token, then validating them on my endpoint, then grant the access, so the token is not stored on the client machine ...Walid Ammar
Right, but correct me if I'm wrong, but isn't this what the JWT token is for?thijsai
If there is no user Auth::user() will return null, right? @thijsaiWalid Ammar
@thijsai In machine-to-machine case, no users associated with tokens.Walid Ammar

9 Answers

15
votes

I use this, to access the authenticated client app...

$bearerToken = $request->bearerToken();
$tokenId = (new \Lcobucci\JWT\Parser())->parse($bearerToken)->getHeader('jti');
$client = \Laravel\Passport\Token::find($tokenId)->client;

$client_id = $client->id;
$client_secret = $client->secret;

Source

10
votes

However the answer is quite late, i got some errors extracting the JTI header in Laravel 6.x because the JTI is no longer in the header, but only in the payload/claim. (Using client grants)

local.ERROR: Requested header is not configured {"exception":"[object] (OutOfBoundsException(code: 0): Requested header is not configured at /..somewhere/vendor/lcobucci/jwt/src/Token.php:112)

Also, adding it in a middleware was not an option for me. As i needed it on several places in my app.

So i extended the original Laravel Passport Client (oauth_clients) model. And check the header as well as the payload. Allowing to pass a request, or use the request facade, if no request was passed.

<?php

namespace App\Models;

use Illuminate\Support\Facades\Request as RequestFacade;
use Illuminate\Http\Request;
use Laravel\Passport\Client;
use Laravel\Passport\Token;
use Lcobucci\JWT\Parser;

class OAuthClient extends Client
{
    public static function findByRequest(?Request $request = null) : ?OAuthClient
    {
        $bearerToken = $request !== null ? $request->bearerToken() : RequestFacade::bearerToken();

        $parsedJwt = (new Parser())->parse($bearerToken);

        if ($parsedJwt->hasHeader('jti')) {
            $tokenId = $parsedJwt->getHeader('jti');
        } elseif ($parsedJwt->hasClaim('jti')) {
            $tokenId = $parsedJwt->getClaim('jti');
        } else {
            Log::error('Invalid JWT token, Unable to find JTI header');
            return null;
        }

        $clientId = Token::find($tokenId)->client->id;

        return (new static)->findOrFail($clientId);
    }
}

Now you can use it anywhere inside your laravel app like this:

If you have $request object available, (for example from a controller)

$client = OAuthClient::findByRequest($request);

Or even if the request is not available somehow, you can use it without, like this:

$client = OAuthClient::findByRequest();

Hopefully this useful for anyone, facing this issue today.

9
votes

There is a tricky method. You can modify the method of handle in the middleware CheckClientCredentials, just add this line.

        $request["oauth_client_id"] = $psr->getAttribute('oauth_client_id');

Then you can get client_id in controller's function:

public function info(\Illuminate\Http\Request $request)
{
    var_dump($request->oauth_client_id);
}
5
votes

The OAuth token and client information are stored as a protected variable in the Laravel\Passport\HasApiTokens trait (which you add to your User model).

So simply add a getter method to your User model to expose the OAuth information:

public function get_oauth_client(){
  return $this->accessToken->client;
}

This will return an Eloquent model for the oauth_clients table

3
votes

So, no answers ...

I was able to resolve the issue by consuming my own API, finally I came up with simpler authentication flow, the client need to send their id & secret with each request, then I consumed my own /oauth/token route with the sent credentials, inspired by Esben Petersen blog post.

Once the access token is generated, I append it to the headers of Symfony\Request instance which is under processing.

My final output like this:

<?php

namespace App\Http\Middleware;

use Request;

use Closure;

class AddAccessTokenHeader
{
    /**
     * Octipus\ApiConsumer
     * @var ApiConsumer
     */
    private $apiConsumer;


    function __construct() {
        $this->apiConsumer  = app()->make('apiconsumer');
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $this->apiConsumer->post('/oauth/token', $request->input(), [
            'content-type' => 'application/json'
        ]);


        if (!$response->isSuccessful()) {
            return response($response->getContent(), 401)
                    ->header('content-type', 'application/json');
        }

        $response = json_decode($response->getContent(), true);

        $request->headers->add([
            'Authorization'     => 'Bearer ' . $response['access_token'],
            'X-Requested-With'  => 'XMLHttpRequest'
        ]);

        return $next($request);

    }
}

I used the above middleware in conjunction with Passport's CheckClientCredentials.

protected $middlewareGroups = [
    'web' => [
        ...
    ],

    'api' => [
        'throttle:60,1',
        'bindings',
        \App\Http\Middleware\AddAccessTokenHeader::class,
        \Laravel\Passport\Http\Middleware\CheckClientCredentials::class
    ],
];

This way, I was able to insure that $request->input('client_id') is reliable and can't be faked.

3
votes

I dug into CheckClientCredentials class and extracted what I needed to get the client_id from the token. aud claim is where the client_id is stored.

<?php
    Route::middleware('client')->group(function() {
        Route::get('/client-id', function (Request $request) {
            $jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $request->header('authorization')));
            $token = (new \Lcobucci\JWT\Parser())->parse($jwt);

            return ['client_id' => $token->getClaim('aud')];
        });
    });

Few places to refactor this to in order to easily access but that will be up to your application

2
votes

As I can see the above answer are old and most importantly it dose not work with laravel 8 and php 8, so I have found a way to get the client id of the access token ( current request )

the answer is basically making a middleware, and add it to all routes you want to get the client id.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Nyholm\Psr7\Factory\Psr17Factory;
use Laravel\Passport\TokenRepository;
use League\OAuth2\Server\ResourceServer;
use Illuminate\Auth\AuthenticationException;
use League\OAuth2\Server\Exception\OAuthServerException;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;

class SetPassportClient
{

    /**
     * The Resource Server instance.
     *
     * @var \League\OAuth2\Server\ResourceServer
     */
    protected $server;

    /**
     * Token Repository.
     *
     * @var \Laravel\Passport\TokenRepository
     */
    protected $repository;

    /**
     * Create a new middleware instance.
     *
     * @param  \League\OAuth2\Server\ResourceServer  $server
     * @param  \Laravel\Passport\TokenRepository  $repository
     * @return void
     */
    public function __construct(ResourceServer $server, TokenRepository $repository)
    {
        $this->server = $server;
        $this->repository = $repository;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        $psr = (new PsrHttpFactory(
            new Psr17Factory,
            new Psr17Factory,
            new Psr17Factory,
            new Psr17Factory
        ))->createRequest($request);

        try {
            $psr = $this->server->validateAuthenticatedRequest($psr);
        } catch (OAuthServerException $e) {
            throw new AuthenticationException;
        }
        
        $token = $this->repository->find($psr->getAttribute('oauth_access_token_id'));

        if (!$token)
            abort(401);

        $request->merge(['passportClientId' => $token->client_id]);

        return $next($request);
    }
}

Add the middleware to app\Http\Kernel.php

protected $routeMiddleware = [
    .
    .
    'passport.client.set' => \App\Http\Middleware\SetPassportClient::class
];

Finaly in the routes add the middleware

Route::middleware(['client', 'passport.client.set'])->get('/test-client-id', function (Request $request){
 dd($request->passportClientId); // this the client id
});

Sorry for the long answer, but I want it to be very clear to any all.

All of the code was inspired by laravel CheckCredentials.php

1
votes

In the latest implementation you can use:

    use Laravel\Passport\Token;
    use Lcobucci\JWT\Configuration;
    
    $bearerToken = request()->bearerToken();
    $tokenId = Configuration::forUnsecuredSigner()->parser()->parse($bearerToken)->claims()->get('jti');
    $client = Token::find($tokenId)->client;

as suggested here: https://github.com/laravel/passport/issues/124#issuecomment-784731969

0
votes
public function handle($request, Closure $next, $scope)
{
    if (!empty($scope)) {
        $psr      = (new DiactorosFactory)->createRequest($request);
        $psr      = $this->server->validateAuthenticatedRequest($psr);
        $clientId = $psr->getAttribute('oauth_client_id');
        $request['oauth_client_id'] = intval($clientId);
       }

    return $next($request);
}

put above to your middleware file, then you can access client_id by request()->oauth_client_id