Courses

Laravel Project PROCESS: From Start to Finish

3rd-Party Providers: Login with Google

Summary of this lesson:
- Implementing Google OAuth integration
- Setting up Socialite configuration
- Adding authentication routes and controllers
- Writing tests for social authentication

The next feature we will work on is Google Login. Our client decided that they want the users to log in via Google. This means we must add Google as a Socialite provider.

Reminder: This course is about the process and not much about the code. For the code, we will use the example from our separate older Tutorial about Google Sign In on Laravel Daily.

The main emphasis in this lesson will be on the process of making that feature work in a multi-server and multi-developer environment: how to set up an external 3rd-party provider to make it work locally and on other servers and write automated tests for it.

The general plan of action is this:

  • Installing Socialite
  • Configuring Google provider
  • Adding Socialite Controller
  • Adding Routes
  • Adding a Button to Login/Registration pages
  • Setting up our project in Google Identity
  • Testing integration locally
  • Writing tests
  • Preparing for Staging/Production environments

Let's get to work!

Note: Just a reminder - as for every new feature, we are creating a GitHub issue and a new branch here, and you should, too.


Installing Socialite

Installing Socialite is as simple as running a composer command:

composer require laravel/socialite

Once that is done, we need to configure it.


Configuring Google Provider

Let's add the Google provider to our config/services.php file.

config/services.php

// ...
 
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT'),
],
 
// ...

Next, we need to create a list of supported Auth providers, as we will be using this to check if the provider is supported:

config/auth.php

// ...
 
'socialite' => [
'drivers' => [
'google',
],
],
 
// ...

From here, we need to add the Google API keys to our .env file:

.env

// ...
 
GOOGLE_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXX
GOOGLE_REDIRECT="${APP_URL}/google/callback"

And lastly, we need some additional fields in our users table:

Migration

Schema::table('users', function (Blueprint $table) {
$table->string('provider_name')->nullable()->after('id');
$table->string('provider_id')->nullable()->after('provider_name');
$table->string('password')->nullable()->change();
$table->string('avatar')->nullable();
});

Don't forget to add these fields to $fillable array in the User model:

User.php

protected $hidden = [
'password', 'remember_token',
'provider_name', 'provider_id', 'password', 'remember_token',
];

That's it! We have configured Socialite and prepared our User model for Google login.


Adding Socialite Controller

Next, we must create a Controller to handle the Socialite login process. Inside, we want to:

  1. Redirect the User to the social provider login page
  2. Handle the callback from the social provider

Let's add one:

Reminder: This is the overview of the code. Again, if you want a deep dive into what the code does - visit our other tutorial

app/Http/Controllers/SocialLoginController.php

use App\Models\User;
use Exception;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Facades\Socialite;
 
class SocialLoginController extends Controller
{
public function redirectToProvider(string $provider): \Symfony\Component\HttpFoundation\RedirectResponse
{
if (! in_array($provider, (array) config('auth.socialite.drivers'), true)) {
abort(404, 'Social Provider is not supported');
}
 
return Socialite::driver($provider)->redirect();
}
 
public function handleProviderCallback(string $provider): RedirectResponse
{
if (! in_array($provider, (array) config('auth.socialite.drivers'), true)) {
abort(404, 'Social Provider is not supported');
}
 
try {
$user = Socialite::driver($provider)->user();
} catch (Exception $e) {
return redirect()->route('login');
}
 
$existingUser = User::where('email', $user->getEmail())->first();
 
if ($existingUser) {
auth()->login($existingUser, true);
} else {
$newUser = new User;
$newUser->provider_name = $provider;
$newUser->provider_id = $user->getId();
$newUser->name = $user->getName() ?? '';
$newUser->email = $user->getEmail() ?? '';
$newUser->email_verified_at = now();
$newUser->avatar = $user->getAvatar();
$newUser->save();
 
auth()->login($newUser, true);
}
 
return redirect()->route('dashboard');
}
}

Adding Routes

Last, we have to register the routes:

routes/auth.php

use App\Http\Controllers\SocialLoginController;
 
// ...
 
Route::middleware('guest')->group(function () {
// ...
 
Route::get('redirect/{provider}', [SocialLoginController::class, 'redirectToProvider'])
->name('social.login')
->where('driver', implode('|', config('auth.socialite.drivers')));
 
Route::get('{provider}/callback', [SocialLoginController::class, 'handleProviderCallback'])
->name('social.callback')
->where('provider', implode('|', config('auth.socialite.drivers')));
});

And that's it! Our logic is now in place, but we still need to add the button to the login page.


Adding Button to Login/Registration Pages

To add the button, we will use a new Blade Component:

resources/views/auth/login.blade.php

{{-- ... --}}
 
<x-primary-button-link
class="mr-2 ml-2"
:href="route('social.login', 'google')">
{{ __('Google Sign In') }}
</x-primary-button-link>
 
<x-primary-button>
{{ __('Log in') }}
</x-primary-button>
 
{{-- ... --}}

And:

resources/views/auth/register.blade.php

{{-- ... --}}
 
<x-primary-button-link
class="mr-2 ml-2"
:href="route('social.login', 'google')">
{{ __('Google Sign In') }}
</x-primary-button-link>
<x-primary-button>
{{ __('Register') }}
</x-primary-button>
 
{{-- ... --}}

Note: Remember to run npm run build, as we have included some new styling.

If we try to load the page, it will cause an error as the primary-button-link component doesn't exist. Let's add it:

resources/views/components/primary-button-link.blade.php

<a {{ $attributes->merge(['class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
{{ $slot }}
</a>

We also added an Avatar to the navbar, so let's add it:

resources/views/layouts/navigation.blade.php

{{-- ... --}}
 
<div>
@if(auth()->user()->avatar)
<img src="{{ auth()->user()->avatar }}" alt="avatar" width="32" height="32"
class="mr-2 inline rounded"/>
@endif
{{ Auth::user()->name }}
</div>
 
{{-- ... --}}
 
<div class="font-medium text-base text-gray-800 dark:text-gray-200">
@if(auth()->user()->avatar)
<img src="{{ auth()->user()->avatar }}" alt="avatar" width="16" height="16"
class="mr-2 inline-block"/>
@endif
{{ Auth::user()->name }}
</div>
 
{{-- ... --}}

Setting Up Google OAuth in Google Identity and Testing Locally

It's time to get API keys from Google and test it locally. This step will be split into two parts:

  1. Local Testing - Using ngrok (or other tools) to expose our local server to the internet
  2. Setting up Google Identity - Creating a new project and adding API keys

Let's start with exposing our local server so that we can get the callback URL and domain:

Exposing Local Server

There are a couple of ways to expose your local app to the internet:

But we are most familiar with ngrok, so we will use that one.

Note: We assume you have ngrok installed on your machine. If you still need to, please follow the official documentation.

To expose our local server, we need to run the following command:

ngrok http --host-header=rewrite https://linksletter.test

Note: linksletter.test should be replaced with your local domain.

Once you run the command, you will see a screen like this:

From there, copy the Forwarding URL and set it as your APP_URL in the .env file:

.env

# ...
 
APP_URL=https://9030-78-58-236-130.ngrok-free.app
 
# ...

Now, you can open that URL in the browser and see your local app.

This will now be our public domain that we will use to set up Google Identity.

Getting OAuth Credentials

To get Google Credentials, we need to visit the Credentials Page in the Google Cloud Console. Then follow these steps:

  1. Click on Create Credentials
  2. Select OAuth client ID
  3. Fill out the OAuth consent screen settings
  4. Select Web application as the Application type
  5. Fill in the form (name)
  6. Add the Authorized redirect URIs - this should be the callback route from our app
    1. In our case, it looks like this https://9030-78-58-236-130.ngrok-free.app/google/callback - in your case, it will have a different domain
  7. Copy the given settings to your .env file
    1. Client ID to GOOGLE_CLIENT_ID
    2. Client Secret to GOOGLE_CLIENT_SECRET

That's it! The application is now set up, and we can test it locally.

To do that, open the Login page and click the Google Sign In button. You should be redirected to the Google login page. After logging in, you should be redirected back to your app and see the dashboard:

It should contain your name and avatar in the navbar.


Writing Tests

Once things are running, we should test that everything works as expected. Let's add a test for our Social Login feature.

With this test, we will use Laravel Facades mocking to swap the Socialite driver with a Custom one using Anonymous Classes:

tests/Feature/Social/GoogleTest.php

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Contracts\Factory;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\User;
 
use function Pest\Laravel\assertAuthenticated;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\followingRedirects;
use function Pest\Laravel\get;
 
uses(RefreshDatabase::class);
 
test('google social login works', function () {
Socialite::swap(new class implements Factory
{
public function driver($driver = null)
{
// Create and return a new Anonymous Class instance
return new class implements Provider
{
public function redirect(): RedirectResponse
{
return new RedirectResponse(route('social.callback', ['provider' => 'google']));
}
 
// Hardcoded user data to be returned from the provider
public function user(): User
{
$user = new User;
$user->name = 'John Doe';
$user->email = '[email protected]';
 
return $user;
}
};
}
});
 
// This allows us to follow the redirect to the callback route and dashboard
followingRedirects();
 
// Initialize the Social Login process
get(route('social.login', ['provider' => 'google']))
->assertStatus(200);
 
// We should see our User created
assertDatabaseHas('users', [
'name' => 'John Doe',
'email' => '[email protected]',
]);
 
// Confirm that the User is authenticated
assertAuthenticated();
});

This test may seem complex, but it's actually pretty simple. The swap part is the scariest.


Preparation for Staging/Production Environments

Now that we have our feature completed, there are a few things to keep track of:

1 - We need to fill our .env.example file with empty keys:

.env.example

# ...
 
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT="${APP_URL}/google/callback"

2 - Create a new Google OAuth application for each environment (staging and production) and fill the .env file with the correct keys.

3 - Test manually on both environments to ensure everything works as expected (since API keys can differ).

And, of course, never share your API keys with anyone OR commit them to the repository.


That's it! We have successfully added Google as a Login method. From here, we should prepare our servers (both staging and production) with API keys for a smooth deployment.

In the next lesson, we will show another 3rd party API example: we will work on creating newsletter issue content using OpenAI API.

Previous: Automated Bug Tracking: Sentry Example

No comments yet…

avatar
You can use Markdown