Search code examples
laravellaravel-passportlaravel-6laravel-6.2

Log in on 3rd party website using Laravel Passport


I need to solve the following problem: I created two applications using Laravel.

  • Application A can be reached via https://test.example.org and provides a passport-instance and all user-data (Model: User).
  • Application B can be reached via https://test.org and uses the API Application A provides using an "api-user" that exists in Application A to fetch "public" data

Now I would like to provide a login method for users of Application B. All users that are using Application B are existing in Application A. So a user logging in on Application B is using the credentials that are saved in Application A. All changes that are made to the user in Application A should be visible on Application B.

Now I wonder how I could do that. Any idea how to solve this? I can imagine using API-calls with

        $client = $this->client;
        try {
            return $client->request('POST',
                'https://'.config('server').'/oauth/token', [
                    'headers'     => [
                        'cache-control' => 'no-cache',
                        'Content-Type'  => 'application/x-www-form-urlencoded',
                    ],
                    'form_params' => [
                        'client_id'     => ...,
                        'client_secret' => ...,
                        'grant_type'    => 'password',
                        'username'      => $users-username,
                        'password'      => $users-password,
                    ],
                ]);
        } catch (BadResponseException $e) {
            return $e->getResponse();
        }
    }

but I don't get, whether I need to create a client_id and client_secret for each user on Application A. If this is the case, I'd need to store these information somewhere so I'd end up creating a User model on Application B itself.

My questions are:

  • How can I log in users that are existing on Application A to Application B the most elegant way?
  • Is it an option to use Laravel/Socialite from the client side (Application B)?

Solution

  • The most elegant way would be to use Laravel Socialite in your Application B to act as an OAuth client for Application A which acts as an OAuth server (using Laravel Passport).

    1. On Application A, you need to create a client on Passport for your Application B:
    php artisan passport:client --password
    

    First, you will be asked Which user ID should the client be assigned to?. Press enter as you don't want to restrict the client to a specific user.

    Then, you will be asked to provide a name for your client, your should enter something like Application B.

    At last, you will be asked for the redirect URL of you client. You should enter https://test.org/oauth/callback, which is the URL that Socialite will use to redirect the users back after they are authenticated.

    If you need to test this on your machine, enter your local domain, e.g. https://application-b.test/oauth/callback.

    Once the client is created, keep the Client ID and Client secret as you will need them later.

    You will also need an API endpoint to provide the details for the authenticated user, .e.g. https://test.example.org/user.

    1. On Application B, after installing Laravel Socialite, you need to set the credentials for the Application A in your services.php configuration file. For example:
    'passport' => [
        'client_id' => env('PASSPORT_CLIENT_ID'),
        'client_secret' => env('PASSPORT_CLIENT_SECRET'),
        'redirect' => 'oauth/callback',
    ],
    

    In your .env file, set the client ID and secret for the Passport client you created in Application A:

    PASSPORT_CLIENT_ID=...
    PASSPORT_CLIENT_SECRET=...
    
    1. Now you need to extend Socialite with a custom provider for Application A.

    Create a PassportProvider class in the app/Socialite directory (or anywhere inside app if you prefer).

    <?php
    
    namespace App\Socialite;
    
    use Laravel\Socialite\Two\AbstractProvider;
    use Laravel\Socialite\Two\ProviderInterface;
    use Laravel\Socialite\Two\User;
    use Illuminate\Support\Arr;
    
    class PassportProvider extends AbstractProvider implements ProviderInterface
    {
        /**
         * {@inheritdoc}
         */
        protected function getAuthUrl($state)
        {
            return $this->buildAuthUrlFromBase('https://test.example.org/oauth/authorize', $state);
        }
    
        /**
         * {@inheritdoc}
         */
        protected function getTokenUrl()
        {
            return 'https://test.example.org/oauth/token';
        }
    
        /**
         * {@inheritdoc}
         */
        protected function getUserByToken($token)
        {
            $response = $this->getHttpClient()->get(
                'https://test.example.org/user',
                $this->getRequestOptions($token)
            );
    
            return json_decode($response->getBody(), true);
        }
    
        /**
         * {@inheritdoc}
         */
        protected function mapUserToObject(array $user)
        {
            return (new User)->setRaw($user)->map([
                'id' => $user['id'],
                'name' => Arr::get($user, 'name'),
                'email' => Arr::get($user, 'email'),
            ]);
        }
    
        /**
         * Get the default options for an HTTP request.
         *
         * @param string $token
         * @return array
         */
        protected function getRequestOptions($token)
        {
            return [
                'headers' => [
                    'Accept' => 'application/json',
                    'Authorization' => 'token '.$token,
                ],
            ];
        }
    }
    

    Next, in your ApplicationServiceProvider (or in a separate service provider), add the following:

    use App\Socialite\PassportProvider;
    use Illuminate\Support\Facades\URL;
    use Laravel\Socialite\Facades\Socialite;
    
    public function boot()
    {
        Socialite::extend('passport', function (function ($app) {
            $config = $app['config']['services.passport'];
    
            return new PassportProvider(
                $app['request'],
                $config['client_id'],
                $config['client_secret'],
                URL::to($config['redirect'])
            );
        });
    }
    

    Now you are ready to use your custom Socialite provider in your login controller on your client application:

    <?php
    
    namespace App\Http\Controllers\Auth;
    
    use App\Http\Controllers\Controller;
    use Laravel\Socialite\Facades\Socialite;
    
    class LoginController extends Controller
    {
        /**
         * Redirect the user to the GitHub authentication page.
         *
         * @return \Illuminate\Http\RedirectResponse
         */
        public function redirectToProvider()
        {
            return Socialite::driver('passport')->redirect();
        }
    
        /**
         * Obtain the user information from GitHub.
         *
         * @return \Illuminate\Http\Response
         */
        public function handleProviderCallback()
        {
            $user = Socialite::driver('passport')->user();
        }
    }
    

    Don't forget to register the login routes in your routes/web.php file:

    Route::get('oauth/redirect', 'Auth\LoginController@redirectToProvider');
    Route::get('oauth/callback', 'Auth\LoginController@handleProviderCallback');