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 (let's assume it is api.com & hosted using vapor)
- A seperate frontend Vue/React client (let's assume it is consumer.com & hosted using now.sh somewhere else)
- First party mobile apps.
What do we want to achieve?
- Build the auth mechanism in our Laravel API
api.com
that allows users to log in and log out of our app from the frontend clientconsumer.com
.
What will we use to achieve that?
- 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.
Password grant tokens vs Personal access token
- The laravel docs explain the difference in a fairly simple language, so I will just paste their definitions here:
The OAuth2 password grant
allows your first-party clients, such as a mobile application, to obtain an access token using an e-mail address / username and password. This allows you to issue access tokens securely to your first-party clients without requiring your users to go through the entire OAuth2 authorization code redirect flow.Personal access
: Sometimes, your users may want to issue access tokens to themselves without going through the typical authorization code redirect flow. Allowing users to issue tokens to themselves via your application's UI can be useful for allowing users to experiment with your API or may serve as a simpler approach to issuing access tokens in general.
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
- Laravel Airlock provides a featherweight authentication system for SPAs and simple APIs. It is still in beta, but password grant seems out of scope for this package at the moment.
- In short, Airlock is great for SPAs but not for auth between your API and first-party clients. Passport has everything we need for a robust auth system, just need to modify or re-use the code ourselves as per the needs.
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:
- 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();
$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));
}