You can now log in using your Google or GitHub account
You can now register and log in to Flare using your Google or GitHub account.
You'll see these two new buttons on the register and login page.
You can log in to Flare using these buttons with a single click. This is way faster than the traditional email / password flow (where we'll also need to verify your email).
In this blog post, I'd like to show you how this feature works behind the scenes. Spoiler: Socialite makes this a breeze.
Implementing social login in a Laravel app
Google and GitHub offer login functionalities through what is called the OAuth flow. This flow will redirect the user from Flare to a login page on Google/GitHub, and after the user has logged in there, Google/GitHub will redirect the user back to Flare, which contains information about the logged-in user. That's the simplified version.
Behind the scenes, Flare is a Laravel app. Let's review the steps we took to add a social login to Flare.
Both Google and GitHub will send ids and tokens around the user the will be logged in. We added some columns in our database to hold that info.
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->after('remember_token', function (Blueprint $table) {
$table->string('github_id')->nullable()->index();
$table->string('google_id')->nullable()->index();
});
});
}
};
Laravel has a great first-party package called Socialite that can handle logins using the OAuth flow.
You can install it using Composer:
composer require laravel/socialite
Next up, you should create an OAuth application at Google and/or GitHub that will provide you with a client_id
and client_secret.
Explaining how you can create this OAuth application is beyond the scope of this post, but you'll probably find it quickly with some Googling.
In the config/services.php
file, we added these keys.
return [
// other values...
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('APP_URL') . '/auth/callback/github',
],
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('APP_URL') . '/auth/callback/google',
],
];
Of course, you should also set the client id and secret as .env
variables; use the names mentioned in the config file above.
Next, let's add two routes to handle the social login.
use App\Http\Auth\SocialiteController;
// in a routes file
Route::get('auth/redirect/{driver}', [SocialiteController::class, 'redirect'])->name('socialite');
Route::get('auth/callback/{driver}', [SocialiteController::class, 'callback']);
When a user visits the first route, the user will be redirected to Google/GitHub. The second route will be hit when Google/GitHub redirects back to Flare. If the request contains the correct information, we'll log in to the user.
Let's take a look at the implementation of the
SocialiteController
that handles requests to both these routes.
namespace App\Http\Auth;
use App\Domain\Team\Actions\FindOrCreateUserForSocialiteAction;
use App\Domain\Team\Enums\SocialiteDriver;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\InvalidStateException;
class SocialiteController
{
public function redirect(string $driver): RedirectResponse
{
abort_if(!in_array($driver, ['github', 'google']), 404);
/** @var \Laravel\Socialite\Two\GoogleProvider|\Laravel\Socialite\Two\GithubProvider $socialite */
$socialite = Socialite::driver($driver);
return match ($driver) {
'github' => $socialite->scopes(['read:user', 'user:email'])->redirect(),
'google' => $socialite->redirect(),
};
}
public function callback(string $driver): RedirectResponse
{
// implementation removed for brevity...
}
}
So in the redirect
function of the controller, we will redirect to Google/GitHub. Socialite will build up the correct URL. Nice!
Next, let's look at the implementation of callback
, which will be called when Google/GitHub redirects back.
<?php
namespace App\Http\Front\Controllers;
use App\Domain\Team\Actions\CreateUserAction;
use App\Domain\Team\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as SocialiteUser;
use Laravel\Socialite\Facades\Socialite;
class SocialiteController
{
public function redirect(string $driver): RedirectResponse
{
// removed for brevity
}
public function callback(string $driver): RedirectResponse
{
abort_if(! in_array($driver, ['github', 'google']), 404);
$socialiteUser = Socialite::driver($driver)->user();
$user = $this->findUserByKeyOrEmail($driver, $socialiteUser);
$user ??= $this->createUser($driver, $socialiteUser);
$this->updateUser($driver, $user, $socialiteUser);
Auth::login($user, remember: true);
return redirect()->route('projects.index');
}
// other functions removed for brevity, well get
// to those.
}
In the above code, you see that we're going to get to the information that Google/GitHub provided by calling Socialite::driver($driver)->user()
. This object contains the email and 3rd party user id of the user at the external service. We will use that information to look up the user with the same email or 3rd party user id in our local data pass. If such a user doesn't exist, well create it. After that, we'll log in the user and redirect to the projects.index
route where all Flare projects for that user are shown.
To finish off, let's look at the implementations of the createUser
and updateUser
functions.
<?php
namespace App\Http\Front\Controllers;
use App\Domain\Team\Actions\CreateUserAction;
use App\Domain\Team\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as SocialiteUser;
use Laravel\Socialite\Facades\Socialite;
class SocialiteController
{
// other functions
protected function findUserByKeyOrEmail(string $driver, SocialiteUser $user): ?User
{
return User::where("{$driver}_id", $user->getId())
->orWhere(function (Builder $query) use ($user) {
$query->where('email', $user->getEmail())->whereNotNull('email_verified_at');
})
->first();
}
protected function createUser(string $driver, SocialiteUser $user): User
{
return app(CreateUserAction::class)->execute(
name: $user->getName(),
email: $user->getEmail(),
password: Str::random(64),
attributes: [
"{$driver}_id" => $user->getId(),
'email_verified_at' => now(),
]
);
}
protected function updateUser(string $driver, User $user, SocialiteUser $socialiteUser): bool
{
$user->fill([
"{$driver}_id" => $socialiteUser->getId(),
]);
if (! $user->email_verified_at && $user->email === $socialiteUser->getEmail()) {
$user->email_verified_at = now();
}
return $user->save();
}
}
The above code should be pretty straightforward.
In closing
In this post, you learned that adding a social login in a Laravel app is pretty easy. Socialite does the heavy lifting for you.
In our sister service, Oh Dear, we see that about half of the log ins and registrations now happen via Google. People seem to like that they can now log in using a single click. Adding this to your app might be worthwhile as well.