Using Laravel Passport with React Native

Dec 4, 2019

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 in User class
  • Declare Passport::routes() in AuthServiceProvider::boot
  • Finally, set “passport” as the guards.api.driver in auth.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 by api/.

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.