API Authentication with LDAP and Laravel Passport

This article takes us through installation and configuration of LDAP and Laravel Passport on a Laravel project. This will enable API authentication with access and refresh tokens using existing Active Directory accounts.

Laravel comes with a fluent auth out of the box. For those wanting to use it as an API that manages its own API authentication, Passport does a fantastic job in that regard. Going further, some enterprise systems will require you to use their existing Active Directory accounts so everyone is saved from having to save gazillion passwords. And yes, Laravel LDAP does that well too.

Wait, it looks like there is no case where this article can come in after all. Maybe not just yet. What if you are required to develop an API for a mobile app but to be used by an enterprise? They require their Active Directory. They memorize one password. And you, on the other hand, require Laravel Passport to manage the app’s authentication with those tokens. Let’s crack this one up!

First, let’s set up the laravel application with

laravel new ldap-passport

Then install the two composer packages

cd ldap-passport
composer require adldap2/adldap2-laravel
composer require laravel/passport

You can find the config details of adaldap installation on their docs

Since traditionally you cannot use LDAP and Passport together out of the box, we will draw their lines in our auth config file and in LoginController so that LDAP handles the authentication with Active Directory, and pass on to Passport to issue tokens to the client.

To achieve this, first we change the api guard to passport and users provider to ldap as in the code snippet below.

<?php

return [

    // ...

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            // Change the driver to 'passport'
            'driver' => 'passport',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

    'providers' => [
        'users' => [
            // Change the driver to 'ldap'
            'driver' => 'ldap',
            'model' => App\User::class,
        ],
    ],
    
    // ...

];

Next we proceed to our login controller. We will attempt to login with our user provider defined above

Auth::attempt(['username' => request('username'), 'password' => request('password'), true)

Then we fetch the passport client to use with the request. We assume (of course we require) the consumers of the API to pass their client’s API key in the request header.

$client = PassportClient::findClientBySecret(request()->header("apiKey"));

And then we use the client to generate API tokens to return to the client

$passport = (new PassportAuthenticator($request))->authenticate($client, request('username'), request('password'));

And finally we have out API tokens!

return response()->json([
    "access_token" => $passport->access_token,
    "expires_in" => $passport->expires_in,
    "refresh_token" => $passport->refresh_token,
], 200),

The complete login controller should like the one below

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\Passport\Authenticator as PassportAuthenticator;
use App\Models\Passport\PassportClient;
use App\User;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse;
use Psr\Http\Message\ServerRequestInterface;

class LoginController extends Controller
{

    use AuthenticatesUsers;

    protected $redirectTo = '/home';

    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    public function username()
    {
        return 'username';
    }

    /**
     * @param ServerRequestInterface $request
     * @param LoginInterface $login
     * @return JsonResponse
     */
    public function login(ServerRequestInterface $request, LoginInterface $login): JsonResponse
    {
        // Attempt logging in with ldap auth provider
        if (!Auth::attempt(['username' => $username, 'password' => $password], true))
            return response()->json(["error" => "The credentials provided do not match our records"], 401);

        if (!request()->header("apiKey")) 
          return response()->json(["error" => "Your client is not allowed on this app"], 401);

        // get the passport client using the API key passed in the request header
        $client = PassportClient::findClientBySecret(request()->header("apiKey"));

        // generate passport tokens
        $passport = (new PassportAuthenticator($request))
            ->authenticate($client, request('username'), request('password'));

        // return the tokens to the client
        return respose()->json([
            "access_token" => $passport->access_token,
            "expires_in" => $passport->expires_in,
            "refresh_token" => $passport->refresh_token,
        ], 200);
    }
}

That sorts us out, right? Not just yet. At this stage, when subsequent requests are made, the API will try to authenticate with auth:api guard and that won’t work. So we will modify the Kernel to pass our custom middleware (which will refer to Passport Middleware anyway). Here is how…

'passport' => \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,

We add that to the routeMiddleware array of our Http Kernel. So instead of using auth:api in our protected routes, we will use passport.

Route::post('blog', 'BlogController@store')-&gt;middleware('passport');

The very last thing, as you might have noticed, we did some abstraction in the Login Controller. There is a little more happening in PassportClient and PassportAuthenticator classes. That was necessary to keep our code concise and focus on the main objective of the tutorial. You can see what’s happening behind the scenes in the snippets below:

<?php

namespace App\Models\Passport;

use Laravel\Passport\Client;

class PassportClient extends Client
{
    public static function findClientBySecret($clientSecret): PassportClient
    {
        return static::where('secret', $clientSecret)->get()->first();
    }
}
<?php

// Credits to @ceekays

namespace App\Models\Passport;


use Laravel\Passport\Http\Controllers\HandlesOAuthErrors;
use Laravel\Passport\TokenRepository;
use Lcobucci\JWT\Parser as JwtParser;
use League\OAuth2\Server\AuthorizationServer;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response as Psr7Response;

class Authenticator
{
    use HandlesOAuthErrors;

    private $tokens;
    private $server;
    private $jwt;
    private $request = null;

    public function __construct(ServerRequestInterface $request)
    {
        $this->jwt = resolve(JwtParser::class);
        $this->server = resolve(AuthorizationServer::class);
        $this->tokens = resolve(TokenRepository::class);
        $this->request = $request;
    }

    public function authenticate(PassportClient $client, $username, $password)
    {
        $request = $this->request->withParsedBody([
            "username" => $username,
            "password" => $password,
            "client_id" => $client->id,
            "client_secret" => $client->secret,
            "grant_type" => "password"
        ]);

        $response = $this->withErrorHandling(function () use ($request) {
            return $this->convertResponse($this->server->respondToAccessTokenRequest($request, new Psr7Response));
        })->content();

        return json_decode($response);
    }

}

Surce: https://medium.com/@saulchelewani/api-authentication-with-ldap-and-laravel-passport-d6f2f3d7c1bb

Comments are closed.