Using Laravel Passport with React Native
This article was originally published on beqode.com with proper code highlighting & style
Let’s add authentication & authorization to our (very) simple Chat app.
Note: Please refer to the previous article for most of our application setup.
Here’s a quick breakdown of what we’re going to achieve here:
- Authentication: Login via Passport (using email/password)
- Authorization: Make our Chat channel, private
Authentication: Laravel Passport
Laravel Passport server issues tokens that we can pass in the Authorization http header in order to keep a session alive. It is a glorified layer built upon the League OAuth2 server.
As always it is a good idea to have a thorough look at the official documentation, but in a nutshell, in order to setup Passport in your application:
$ composer require laravel/passport
$ php artisan migrate
$ php artisan passport:install
- Use
HasApiTokens
inUser
class - Declare
Passport::routes()
inAuthServiceProvider::boot
- Finally, set
passport
as theguards.api.driver
inauth.php
config file
Password Grant Tokens
This type of tokens are delivered upon successful login (email/password) in our Laravel application. These are the obvious choice if you want to build a Login screen in your React Native application.
From the doc:
The OAuth2 password grant allows your other first-party clients, such as a mobile application, to obtain an access token using an e-mail address / username and password.
Conveniently, when we ran the passport:install
command, Passport has generated for us an OAuth “Password Grant” client (passport:client
).
Note: An OAuth client is an “Application” that may be authorized to access user’s information, in our case, the OAuth client is our Laravel backend (querying the OAuth server).
We can find the password grant client details in the database under the oauth_clients
table. Look for the row where the name
is “Laravel Password Grant Client” and copy the id
and secret
into your local .env
file:
OAUTH_PWD_GRANT_CLIENT_ID=2
OAUTH_PWD_GRANT_CLIENT_SECRET=BsKJfdS3tdPFkPdCTy9AtEc2yA3MymMr7UpMyjgw
AuthController
$ php artisan make:controller Api/AuthController
The AuthController
sole role is to privately query the local OAuth server (Passport).
Note: Since we don’t want to store the “Password Grant” client ID & secret inside the React Native app, we use
AuthController
as a proxy to the local OAuth server. As a general rule of thumb, client ID & secret should always be stored server-side.
<?php
// chat-server/app/Http/Controllers/Api/AuthController.php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class AuthController extends Controller
{
public function login(Request $request)
{
$email = $request->input('email');
$password = $request->input('password');
$http = new \GuzzleHttp\Client();
try
{
$response = $http->post('http://127.0.0.1:8001/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => env('OAUTH_PWD_GRANT_CLIENT_ID') ,
'client_secret' => env('OAUTH_PWD_GRANT_CLIENT_SECRET'),
'username' => $email,
'password' => $password,
'scope' => ''
],
]);
$tokens = json_decode((string)$response->getBody() , true);
}
catch(\GuzzleHttp\Exception\ClientException $e)
{
if ($e->getResponse()->getStatusCode() === 401) {
return response()->json('Invalid email/password combination', 401);
}
throw $e;
}
return response()->json($tokens);
}
}
// chat-server/routes/api.php
Route::post('/login', 'Api\AuthController@login');
Note: Since we declare our
login
route inroutes/api.php
, theapi
middleware group will be applied and the URL will be prefixed byapi/
.
Authorization: Chat private channel
Private channels need authorization to know if a user can access them or not. Such authorization is seamlessly handled between Laravel Echo, laravel-echo-server and our Laravel backend. All we need is a bit of configuration.
By default, the broadcasting authorization routes use the web
middleware group, we want to use the api
middleware group instead.
// chat-server/app/Providers/BroadcastServiceProvider.phpBroadcast::routes([
'middleware' => ['api'],
]);
Chat private channel
What defines a private channel, is the ability for an authenticated user to join it or not. In our case, we want to authorize joining if the user is part of the chat room.
Also, we want to use our freshly setup api
authentication guard (Passport), instead of the default web
guard.
<?php
// chat-server/routes/channels.php
use App\Chat;
use App\User;
Broadcast::channel('chats.{chat}', function (User $user, Chat $chat) {
return $user->chats->contains($chat);
}, ['guards' => ['api']]);
// chat-server/app/Providers/RouteServiceProvider.php
Route::model('chat', Chat::class);
// chat-server/app/User.php
public chats()
{
return $this->belongsToMany(Chat::class);
}
Note: We’re also using explicit model route binding for clarity.
laravel-echo-server
When requesting to join a private channel, our socket server will query our Laravel backend for authorization.
// laravel-echo-server.json{
"authHost": "http://127.0.0.1:8000",
"authEndpoint": "/broadcasting/auth", // ...
}
Laravel Echo (Client)
We’re going to simulate a successful login in order to set the access token in the auth.headers
of our Echo client.
// ChatClient/App.js
const echo = new Echo({
broadcaster: 'socket.io',
host: 'http://127.0.0.1:6001',
client: socketio,
});
// Simulate login
fetch('http://127.0.0.1:8000/api/login', {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'john@example.org',
password: 'john_pass',
}),
})
.then(res => res.json())
.then(tokens => {
// https://github.com/laravel/echo/issues/26#issuecomment-370832818
echo.connector.options.auth.headers.Authorization = 'Bearer ' + tokens.access_token;
echo
.private('chats.1')
.listen('ChatMessageCreated', ev => console.log(ev.message.text));
});
Few things to notice here:
- The
Bearer
Authorization header will be sent by the Echo client, and then forwarded by the socket server to our Laravel backend - Echo is now listening to a
private
channel - john@example.org account has been created in previous article
Note: Interestingly enough, you don’t need to specify the
authEndpoint
Echo client option, since it’s already specified in the socket server configuration. The reason this option is required in a default Laravel setup, is because Pusher is the default driver, and in that case, the Echo client is doing the authorization request.
Running the demo
Even more stuff to run cause if we refer to our previous article:
$ php artisan serve$ php artisan queue:work$ laravel-echo-server start$ react-native run-ios
One additional thing we need to do here, is to run an additional development PHP server, one dedicated to our local OAuth server.
Only the keen eye has noticed that in our
AuthController
above, we’ve specified port8001
.
The reason being, if we request the token oauth/token (POST)
on the same PHP server (port 8000
), it will kill the initial request api/login (POST)
, since the development PHP server is single threaded…
$ php artisan serveLaravel development server started: <http://127.0.0.1:8000>[Wed Dec 4 18:33:37 2019] Failed to listen on 127.0.0.1:8000 (reason: Address already in use)Laravel development server started: <http://127.0.0.1:8001>
Now we’re all setup, let’s create a ChatMessage
in our private chat room.
$ php artisan tinker>>> ChatMessage::create(['chat_id' => 1, 'user_id' => 1, 'text' => 'Hello (again) World']);
If everything goes well, you should see Hello (again) World
printed in the Developer console
of your React Native app!