Using Laravel Passport with React Native
Dec 4, 2019Let’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 the
guards.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
column 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.
// 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 in
routes/api.php
, the “api” 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.php
Broadcast::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.
// 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: "[email protected]",
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 -
[email protected]
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.
Note
Only the keen eye has noticed that in our AuthController above, we’ve specified port 8001.
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 serve
Laravel 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!
If you enjoyed this article, give it a clap on Medium.