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.comGOOGLE_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXGOOGLE_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:
- Redirect the User to the social provider login page
- 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:
- Local Testing - Using ngrok (or other tools) to expose our local server to the internet
- 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:
- Click on
Create Credentials
- Select
OAuth client ID
- Fill out the OAuth consent screen settings
- Select
Web application
as the Application type - Fill in the form (name)
- Add the
Authorized redirect URIs
- this should be thecallback
route from our app- 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
- In our case, it looks like this
- Copy the given settings to your
.env
file-
Client ID
toGOOGLE_CLIENT_ID
-
Client Secret
toGOOGLE_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'; 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', ]); // 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.
No comments yet…