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

RichardStyles's avatar

API authentication (with passport)

I have created a API Authentication controller, which logs in the user to the server and returns the API tokens using the "password" grant_type, that OAuth allows. As I'm testing ideas for a external App which will need to login using normal credentials, then use OAuth2 for it's API feed.

It uses two ENV which are the secrets for the Laravel passport OAuth api; API_CLIENT_ID & API_CLIENT_SECRET. from the "Laravel Password Grant Client" that passport generates (in your DB). As I do not want these to be exposed to the client side.

There was a bit of hassle getting the wrapper request to work and merge in these secrets. However it finally works, but I feel there might be a better way to call the oauth/token route.

  • Install Laravel 5.3
  • Install Auth scaffold
  • Install Laravel Passport
  • Add a new user
  • Remove Auth scaffold
  • Add ApiLoginController to web route (as POST)
Route::post('/login', 'ApiLoginController@login');

The ApiLoginController extends the existing AuthenticatesUsers and the API token request is called after a successful login.

I have yet to see how login failures are handled and directed, but wanted to review the actual login methodology and if anyone can see any improvements.

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;

use Illuminate\Support\Facades\Route;

class ApiLoginController extends Controller
{
    use AuthenticatesUsers;
    
    /**
     * The user has been authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  mixed  $user
     * @return mixed
     */
    protected function authenticated(Request $request, $user)
    {
        $email = $request->input('email');
        $password = $request->input('password');
        $request->request->add([
            'username' => $email,
            'password' => $password,
            'grant_type' => 'password',
            'client_id' => env('API_CLIENT_ID'),
            'client_secret' => env('API_CLIENT_SECRET'),
            'scope' => '*'
        ]);

        $tokenRequest = Request::create(
            env('APP_URL').'/oauth/token',
            'post'
        );
        return Route::dispatch($tokenRequest)->getContent();
    }

}

0 likes
15 replies
adiachenko's avatar

@RichardStyles Great idea, but I don't really understand why you need Auth scaffolding and AuthenticatesUsers trait for this. There is also no need to store client_id and client_secret in .env file because in most cases you will be using a default Password Grant client. Here is more elegant way to achieve the same result.

routes/api.php

Route::post('auth/token', 'Api\Auth\DefaultController@authenticate');
Route::post('auth/refresh', 'Api\Auth\DefaultController@refreshToken');

app/Http/Controllers/Api/Auth/DefaultController.php

<?php

namespace App\Http\Controllers\Api\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;

class DefaultController extends Controller
{
    /**
     * @var object
     */
    private $client;

    /**
     * DefaultController constructor.
     */
    public function __construct()
    {
        $this->client = DB::table('oauth_clients')->where('id', 2)->first();
    }

    /**
     * @param Request $request
     * @return mixed
     */
    protected function authenticate(Request $request)
    {
        $request->request->add([
            'username' => $request->username,
            'password' => $request->password,
            'grant_type' => 'password',
            'client_id' => $this->client->id,
            'client_secret' => $this->client->secret,
            'scope' => '*'
        ]);

        $proxy = Request::create(
            'oauth/token',
            'POST'
        );

        return Route::dispatch($proxy);
    }

    /**
     * @param Request $request
     * @return mixed
     */
    protected function refreshToken(Request $request)
    {
        $request->request->add([
            'grant_type' => 'refresh_token',
            'refresh_token' => $request->refresh_token,
            'client_id' => $this->client->id,
            'client_secret' => $this->client->secret,
        ]);

        $proxy = Request::create(
            '/oauth/token',
            'POST'
        );

        return Route::dispatch($proxy);
    }
}
4 likes
RichardStyles's avatar

@adiachenko The Auth scaffold was added initially to make sure I installed passport right. As I've only loaded it up once before. I also used the AuthenticatesUsers trait as I intend to send down the user details with the request, saving a further api call. Which isn't in the above version. As I'll be coding a front end in Sencha's ExtJS javascript framework, and trying to condense the api feeds as much as I can. There is an advantage to use the AuthenticatesUsers as I can then also leverage the login throttles and password reset requests going forward.

My request struggled getting the username and email, hence the extra lines to set a variable. I suspect that was down to an earlier issue I was finding, so will try and tidy that bit up. I did think about loading up the client details like you have

$this->client = DB::table('oauth_clients')->where('id', 2)->first();

But thought there may be instances where the client is not 2. Granted I can't think of any now. I also assume the env is a few ms faster too without the DB call. There's a couple of bits I can organise better, need to test it without the app_url and check how it handles with CORS.

I've yet to implement the token refresh, as I'm working on a test front end in ExtJS. Mostly as I want to make sure I can get a login right and inject the Bearer Header into any API calls.

Good to see different ways, as everyone will have different scopes to fulfil -for those that find this post. As I must have read a dozen or so posts asking how to do this.

This code was me kicking an idea around and I have a Github Project for this code and any future changes I tryout.

Riari's avatar

I've been toying with this idea and ended up taking a slightly different approach, albeit using the same principle. Here's what I did:

1) Define middleware that adds the client credentials to password grant requests

I call mine password-grant

In App\Http\Kernel::$routeMiddleware:

'password-grant' => \App\Http\Middleware\InjectPasswordGrantCredentials::class,

In App\Http\Middleware\InjectPasswordGrantCredentials:

<?php

namespace App\Http\Middleware;

use DB;
use Closure;

class InjectPasswordGrantCredentials
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        if ($request->grant_type == 'password') {
            $client = DB::table('oauth_clients')
                        ->where('id', config('auth.password_grant_client_id'))
                        ->first();

            $request->request->add([
                'client_id' => $client->id,
                'client_secret' => $client->secret,
            ]);
        }

        return $next($request);
    }
}

(Note that I'm pulling the client ID from config there)

2) Override Passport's token issuing and refresh routes

The route definitions should be identical to the original ones, just with the custom middleware added. I do this after Passport::routes() in AuthServiceProvider::boot().

Route::post('oauth/token', [
    'middleware' => 'password-grant',
    'uses' => '\Laravel\Passport\Http\Controllers\AccessTokenController@issueToken'
]);
Route::post('oauth/token/refresh', [
    'middleware' => ['web', 'auth', 'password-grant'],
    'uses' => '\Laravel\Passport\Http\Controllers\TransientTokenController@refresh'
]);

Same result, but I feel like this is a little bit more elegant.

8 likes
adiachenko's avatar

@Riari Yeah, that's kinda what I did before discovering the topic starter's idea, but I've come to conclusion that it's a bit too obscure semantically. There is no clear indication to what's happening unless you look into middleware code. I find the explicit approach with separate route a bit better.

Riari's avatar

Fair enough :) personally I think it's a bit less obscure since it uses a conventional request lifecycle rather than internally dispatching a request - but the solution is almost identical, it's just that the crucial part is done via middleware instead of a controller action, so it does come down to a matter of preference.

As a general sidenote for anyone who ends up here from searching, there's also an ongoing discussion about the problem here: https://github.com/laravel/passport/issues/165

lara30453's avatar

I have also been on a mission to solve this issue!

I wrote about it here, and provided some possible implementations: Post.

Also, why are you hiding the Client ID? it does not matter if that is shown in the sources code. The only thing that should not be shared is the Client Secret. Plus, it makes it easier to send the Client ID with the request as then multiple first party apps can use it, rather than storing a single one in the .env file.

1 like
Riari's avatar

@joshgallagher24 In my case it's just because I don't expect to use more than one Password Grant client. Of course, if that changes, modifying it to grab a client ID from the request is pretty trivial.

lara30453's avatar

Hello all,

The PR I was trying to push through was closed. here it is: PR.

However, I will try and create a package out of this implementation! I'm not sure when I will have time due to Uni.

Do you all agree that this should be a package, or should we self implement this functionality on a per project basis?

Also, with all of your implementations you should consider returning the tokens in a cookie to better the security.

adiachenko's avatar

@joshgallagher24 I agree that this should be a package, but the thing I wish to point out is that people will need a thorough comment in the docs about how to set up Laravel and their development environment to rely on cookie in server-client communication. I think a typical developer will have a Laravel (with it's default config('session.domain') value) on a guest machine (i.e. Homestead) and public client running on something akin to webpack-dev-server on a host machine. I can pretty much already guess where people will stumble in trying to make this work.

lara30453's avatar

@adiachenko I've been speaking with Alex Bilbie - the creator of PHP's OAuth2 Lib. I clarified with him that, firstly using the Password Grant for a first party SPA was correct which he agreed upon. However, I am awaiting his reply on the cookie situation.

I am in two minds about the cookie use, because in either situation - cookie or no cookie we still create angles of attack through CSFR and XSRF.

I am still messing around with this concept, and I will push my project up soon so you all can give feedback and PR's. I am, also hoping that I can have a package done in the coming weeks to support this functionality.

Finally, to address what you have said are you referring to cross domain cookies?

nickkuijpers's avatar

@joshgallagher24, have u had any response yet from Alex? About the cookie or localstorage?

Does anyone have new experiences about this workflow?

ano's avatar

@adiachenko thanks for approach its working for me. could you provide me next step how mobile app request user data and how to use access_token to get response from laravel application without using vue.js. Thanks in advance.

besrabasant's avatar

This approach gives me an error while running phpunit tests

{"error":"unsupported_grant_type","message":"The authorization grant type is not supported by the authorization server.","hint":"Check the `grant_type` parameter"}"
zivda3rd's avatar

@besrabasant Not sure you got answered. In my experience I find that if you don't structure the data sent in the post correctly, you get that error. eg:

//
$data     = [
        'form_params' => [
            'username'      => $email,
            'password'      => 'secret',
            'client_id'     => env('PASSWORD_CLIENT_ID'),
            'client_secret' => env('PASSWORD_CLIENT_SECRET'),
            'grant_type'    => 'password',
            'scope'         => '*',
        ]
    ];

Please or to participate in this conversation.