Setting up Auth between your Laravel API & the frontend JS client using Passport
Harish Toshniwal • October 10, 2019
original laravelLaravel 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
- An API built using Laravel
- A seperate frontend Vue/React client
What do we want to achieve?
- Build the auth mechanism in our API that allows users to log in and log out of our app from the frontend client.
How to achieve that and what will we use?
- We will use Laravel Passports ‘Password Grant’ feature to achieve that. “Err, wait what is this ‘Password Grant’? ”
- The Password grant type is one of the OAuth grant types that is used by first-party clients to exchange a user's credentials like the email & password for an access token.
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());
}
}
}
}
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(Request $request)
{
$http = new Client;
try {
$response = $http->post(config('services.passport.login_endpoint'), [
'form_params' => [
'grant_type' => 'refresh_token',
'client_id' => config('services.passport.client_id'),
'client_secret' => config('services.passport.client_secret'),
'refresh_token' => $request->refreshToken,
]
]);
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 refresh token is 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:
- The great thing about Laravel and all its first party resources is that their code is beautifully & logically architected and abstracted. That makes resuing or refrencing their code really easy.
- So, rather than making that http call in that controller method we can directly use Passport’s code to generate and return the access & refresh token.
- Basically what Passport was doing in its own controller, we are doing the same with a new controller just to add the client id & secret to the request in between.
- We can achieve this by making Passport’s own controller as the parent class of our new controller and refrencing the
issueToken
method of passports controller in our controller. - The code of the
login()
method of ourPassportAuthController
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();
$parsedBody['grant_type'] = 'password';
$parsedBody['client_id'] = config('services.passport.client_id');
$parsedBody['client_secret'] = config('services.passport.client_secret');
return parent::issueToken($request->withParsedBody($parsedBody));
}
}
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:
public function logout()
{
auth()->user()->tokens->each->delete();
return response()->json('Logged out', 200);
}
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
That’s all, feel free to let me know your thoughts about this on my twitter.