Setting up Auth between your Laravel API & the frontend JS client using Passport

Harish Toshniwal • October 10, 2019

original laravel

Laravel Passport is an official Laravel package to implement API authentication in your Laravel apps. It provides a full OAuth2 server implementation for your Laravel applications.

Things we have

What do we want to achieve?

What will we use to achieve that?

Password grant tokens vs Personal access token

So since now its clear that the Password Grant is the grant we should use in this scenario, let's go ahead with that.

A Guide To OAuth 2.0 Grants by Alex Bilbie

Laravel Passport vs Laravel Airlock

Lets get rolling with Passport

First, install and configure Passport as suggested in the docs. Next, we need to post the client_id & client_secret along with the user’s email & password to passports /oauth/token route to get the access_token & refresh_token. But there’s a problem! Where do we store the client_id & client_secret ? We cannot store them on the frontend since it will be exposed and won’t be secure. client_id is fine, but we can’t store the client_secret

The solution is we ask the frontend to post only the users email and password and we somehow add the client_id & client_secret to the request on the backend in the API, thereby not exposing them on the frontend. There are 2 ways to do that and I will show you both.

This first approach is not what I would recommend, am showing you this approach since it’s the most common solution available on the internet

First is to use a custom route/endpoint pointing to a new controller. Eg: Route::post(‘/login’, ‘PassportAuthController@login’);. Now, we need to ask our frontend client to post to this new route instead of passports /oauth/token route. In the login() method of the new PassportAuthController we will access the request, add the client_id & client_secret to it and delegate the request to passports /oauth/token route using guzzle and making an http call within the controller method itself. The controller may look like this:

class PassportAuthController
{
    public function login(Request $request)
    {
        $http = new GuzzleHttp\Client;

        try {
            $response = $http->post(config('services.passport.login_endpoint'), [
                'form_params' => [
                    'grant_type' => 'password',
                    'client_id' => config('services.passport.client_id'),
                    'client_secret' => config('services.passport.client_secret'),
                    'username' => $request->email,
                    'password' => $request->password,
                ]
            ]);

            return $response->getBody();
        } catch (BadResponseException $e) {
            if ($e->getCode() === 400) {
                return response()->json('Invalid Request', $e->getCode());
            } elseif ($e->getCode() === 401) {
                return response()->json('Your credentials are incorrect. Please try again', $e->getCode());
            }
        }
    }
}

The problem with this approach is that it makes an additional http call which isn’t technically needed. I showed you this approach since it’s the most common solution available on the internet. The second approach which I would suggest to use and what we are using at Jogg is as follows:

class PassportAuthController extends AccessTokenController
{
    public function __construct(AuthorizationServer $server,
                                TokenRepository $tokens,
                                JwtParser $jwt)
    {
        parent::__construct($server, $tokens, $jwt);
    }

    public function login(ServerRequestInterface $request)
    {
        $parsedBody = $request->getParsedBody();

        $client = $this->getClient($parsedBody['client_name']);

        $parsedBody['username'] = $parsedBody['email'];
        $parsedBody['grant_type'] = 'password';
        $parsedBody['client_id'] = $client->id;
        $parsedBody['client_secret'] = $client->secret;

        // since it is not required by passport
        unset($parsedBody['email'], $parsedBody['client_name']);

        return parent::issueToken($request->withParsedBody($parsedBody));
    }

    private function getClient(string $name)
    {
        return Passport::client()
            ->where([
                ['name', $name],
                ['password_client', 1],
                ['revoked', 0]
            ])
            ->first();
    }
}

This way we no longer need to make an extra http call in our code.

You can use the following code to implement the logout functionality on per client basis i.e. logout the client only from the iOS app if the request was made from there:

public function logout()
{
    $client = $this->getClient($request->client_name);

    $token = auth()->user()
        ->tokens
        ->where('client_id', $client->id)
        ->first();

    abort_if(is_null($token), 400, 'Token for the given client name does not exist');

    $token->delete();

    return response()->json('Logged out successfully', 200);
}

Logging in the user after first time registration/sign-up is a common UX requirement. I will share how to do that with passport in an upcoming blog post. That’s all, feel free to let me know your thoughts about this on twitter.

Two most important points:

1: Remember, to make the logout endpoint protected using passports auth:api middleware

2: Since, we will be storing the access & refresh token in an http only cookie to pass it along with every request in the Authorization header, please have proper CORS in place to protect your API against CSRF attacks. We recommend using Barry vd. Heuvel’s laravel-cors package to implement that

Bonus:

You can also add a new route for refreshing the token, that will have the same mechanics as the login() method, just the grant_type will be refresh_token and it won’t include the email & password but the refresh_token received from the /login request. The route and controller method might look like this for it:

Route::post(‘/refresh-token’, ‘PassportAuthController@refresh’);

Controller method:

public function refresh(ServerRequestInterface $request)
{
    $parsedBody = $request->getParsedBody();

    $client = $this->getClient($parsedBody['client_name']);

    $parsedBody['grant_type'] = 'refresh_token';
    $parsedBody['client_id'] = $client->id;
    $parsedBody['client_secret'] = $client->secret;

    // since it is not required by passport
    unset($parsedBody['client_name']);

    return parent::issueToken($request->withParsedBody($parsedBody));
}