Using Laravel Socialite for both authentication and linking accounts

Published in Programming on Feb 14, 2021

It's easy to use Socialite for linking accounts, it's easy to use it for registration, but it gets a bit tricky if you want to do both. Here's how to do it.

We only want to use one OAuth2 (e.g. GitHub) application, because we don't want users to have to grant access to two separate applications.

But an application can only have one callback URL.

So, we'll build exactly that. A single callback route.

Route::get('/auth/github', function () {
    return Socialite::driver('github')->redirect();
})->name('auth.github');

Route::get('/auth/github/callback', function () {
    try {
        $github = Socialite::driver('github')->user();
    } catch (InvalidStateException $exception) {
        session()->increment('github_socialite_attempt_count');

        if (session('github_socialite_attempt_count') > 2) {
            return redirect('/');
        }

        return redirect(route('auth.github'));
    }

    // If a user is logged in, we link the GH account
    if (auth()->check()) {
        /** @var User $user */
        $user = auth()->user();

        if ($user->isUser()) {
            $user->linkGitHub($github->token, $github->getId(), $github->getNickname());

            return redirect(route('profile.show'));
        }
    }

    // If a user is not logged in, but exists, we log him in
    if ($github->getId() && $user = User::firstWhere('github_id', $github->getId())) {
        Auth::login($user, true);

        return redirect(route('dashboard.user'));
    }

    // If no user is found, we create a new account

    /** @var User user */
    $user = User::create([
        'name' => $github->getName() ?? $github->getNickname(),
        'email' => $github->getEmail(),
        'email_verified_at' => now()->subMinute(),
        'profile_photo_path' => "https://avatars3.githubusercontent.com/u/{$github->getId()}?v=4",
    ]);

    $user->linkGitHub($github->token, $github->getId(), $github->getNickname());

    Auth::login($user, true);

    return redirect(route('dashboard.user'));
})->name('auth.github.callback');

Two things:

  1. I'm using Jetstream and I customized my getProfilePhotoUrl() logic. If you use the code above, you should tweak it to match your User model setup.
  2. GitHub auth is sometimes weird and results in an "invalid state" exception, which renders as 500 in production. But it works on the second attempt. So to fix that, we just store a counter and redirect the user back to the auth route if there's an invalid state exception — for a maximum of three times.

And the method for linking accounts:

public function linkGitHub(string $token, int $id, string $username): void
{
    $this->update([
        'github_token' => $token,
        'github_id' => $id,
        'github_username' => $username,
    ]);
}

public function unlinkGitHub(): void
{
    $this->update([
        'github_token' => null,
        'github_id' => null,
        'github_username' => null,
    ]);
}

public function hasLinkedGitHub(): bool
{
    return $this->github_id !== null;
}

And that's all there's to it. Now just create a button saying "Link account", make it redirect the user to /auth/github, and it will link their account.

Similarly, create a button saying "Login with GitHub", make it redirect the user to the same route, and it will log them in, or create their account.

Newsletter

You will be notified whenever I publish an article about the topics you selected. Unsubscribe anytime.

Comments

Your comment will appear once it's approved (to fight spam).