Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log in using GitHub #680

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ LEMON_SQUEEZY_PATH=
GITHUB_PERSONAL_ACCESS_TOKEN=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URI=
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that there are now two different callbacks, the redirect URL is not loaded from the config, it is set up dynamically in each of the redirecting endpoints


SPONSORS_GITHUB_USERNAMES=
SPONSORS_GITHUB_COMPANY_USERNAMES=
Expand Down
55 changes: 55 additions & 0 deletions app/Http/Controllers/Auth/GitHubLoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Models\User;
use App\Services\GitHub;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\GithubProvider;

final readonly class GitHubLoginController
{
/**
* Handles the GitHub login redirect.
*/
public function index(): RedirectResponse
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the endpoint called from the "Log in using github" in the login page, it redirects you to GitHub's authentication page, setting the right callback url

{
/** @var GithubProvider $driver */
$driver = Socialite::driver('github');

return $driver->redirectUrl(route('profile.connect.github.login.callback'))->redirect();
}

/**
* Handles the GitHub login callback.
*/
public function update(Request $request, GitHub $github): RedirectResponse
{
$githubUser = Socialite::driver('github')->user();

$user = User::where('email', $githubUser->getEmail())->first();
if (! $user instanceof User) {
session([
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no user with the email sent back by GitHub, we redirect the user to the "register" page, and we save in the session the github email and the username so that they can be used in that page

'github_email' => $githubUser->getEmail(),
'github_username' => $githubUser->getNickname(),
]);

return to_route('register');
}
if ($user->github_username === null) {
$errors = $github->linkGitHubUser($githubUser->getNickname(), $user, $request);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user with the GitHub email is not yet linked to this GitHub account, we try to link it


if ($errors !== []) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error will only happen in a very unusual situation: if there is already a user linked to this GitHub account but the email in the Pinkary account does not match the email in the GitHub account. In this case we cannot link the user or log them in, so we redirect back to login with an error

return to_route('login')->withErrors($errors, 'github');
}
}
Auth::login($user);

return to_route('home.feed');
}
}
30 changes: 28 additions & 2 deletions app/Http/Controllers/Auth/RegisteredUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
use App\Rules\Recaptcha;
use App\Rules\UnauthorizedEmailProviders;
use App\Rules\Username;
use App\Services\GitHub;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
Expand All @@ -24,16 +26,27 @@
*/
public function create(): View
{
return view('auth.register');
$githubEmail = session()->pull('github_email');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two fields may have been set by the GitHub login process and are used in the register template

$githubUsername = session()->pull('github_username');

return view('auth.register')->with('githubEmail', $githubEmail)->with('githubUsername', $githubUsername);
}

/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
public function store(Request $request, GitHub $gitHub): RedirectResponse
{
$githubUsername = $request->github_username;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we receive a GitHub username, we save again these two fields in the session in case there is a validation error and the user is redirected back to the register page

if ($githubUsername) {
session([
'github_email' => $request->email,
'github_username' => $request->github_username,
]);
}

$request->validate([
'name' => ['required', 'string', 'max:255'],
'username' => ['required', 'string', 'min:4', 'max:50', 'unique:'.User::class, new Username],
Expand All @@ -43,13 +56,26 @@ public function store(Request $request): RedirectResponse
'g-recaptcha-response' => app()->environment('production') ? ['required', new Recaptcha($request->ip())] : [],
]);

$request->session()->forget(['github_email', 'github_username']);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the validation was successful, we don't need these two fields in the session any longer


$user = User::create([
'name' => $request->name,
'email' => $request->email,
'username' => $request->username,
'password' => Hash::make($request->string('password')->value()),
]);

if (is_string($githubUsername) && $githubUsername !== '') {
$errors = $gitHub->linkGitHubUser($githubUsername, $user, $request);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to link the new user to their GitHub account

if ($errors !== []) {
$user->delete();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will happen if there is already a user linked to the GitHub account but their email does not match the email in GitHub. In this case, we cannot register the user linked to the GitHub account, so we remove the recently created user and redirect them back to the register page so that they can try to register again without linking to GitHub


return to_route('register')->withErrors($errors, 'github');
}
$user->email_verified_at = Carbon::now();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user registered using a GitHub account, we can mark the email as verified as GitHub will already have taken care of verifying this email

$user->save();
}

event(new Registered($user));

Auth::login($user);
Expand Down
49 changes: 14 additions & 35 deletions app/Http/Controllers/UserGitHubUsernameController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
namespace App\Http\Controllers;

use App\Jobs\SyncVerifiedUser;
use App\Jobs\UpdateUserAvatar;
use App\Models\User;
use App\Services\GitHub;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\GithubProvider;

final readonly class UserGitHubUsernameController
{
Expand All @@ -20,51 +19,31 @@
*/
public function index(): RedirectResponse
{
return type(Socialite::driver('github')->redirect())->as(RedirectResponse::class);
/** @var GithubProvider $driver */
$driver = Socialite::driver('github');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case we also set the URL dynamically


return $driver->redirectUrl(route('profile.connect.github.update'))->redirect();
}

/**
* Handles the GitHub connection update.
*/
public function update(Request $request): RedirectResponse
public function update(Request $request, GitHub $github): RedirectResponse
{
$githubUser = Socialite::driver('github')->user();

$user = type($request->user())->as(User::class);

try {
$validated = Validator::validate([
'github_username' => $githubUser->getNickname(),
], [
'github_username' => ['required', 'string', 'max:255', 'unique:users,github_username'],
], [
'github_username.unique' => 'This GitHub username is already connected to another account.',
]);
} catch (ValidationException $e) {
if ($githubUser->getNickname() === $user->github_username) {
session()->flash('flash-message', 'The same GitHub account has been connected.');

return to_route('profile.edit');
}

return to_route('profile.edit')->withErrors($e->errors(), 'verified');
}
$githubUsername = $githubUser->getNickname();

$user->update($validated);
$errors = $github->linkGitHubUser($githubUsername, $user, $request);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We moved all this code to a service so that it can be reused in the other two cases where we want to link a user to their GitHub account


SyncVerifiedUser::dispatchSync($user);

$user = type($user->fresh())->as(User::class);

$user->is_verified
? session()->flash('flash-message', 'Your GitHub account has been connected and you are now verified.')
: session()->flash('flash-message', 'Your GitHub account has been connected.');
if ($errors !== []) {
if ($githubUsername === $user->github_username) {
session()->flash('flash-message', 'The same GitHub account has been connected.');
}

if (! $user->is_uploaded_avatar) {
UpdateUserAvatar::dispatch(
user: $user,
service: 'github',
);
return to_route('profile.edit')->withErrors($errors, 'verified');
}

return to_route('profile.edit');
Expand Down
55 changes: 55 additions & 0 deletions app/Services/GitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
namespace App\Services;

use App\Exceptions\GitHubException;
use App\Jobs\SyncVerifiedUser;
use App\Jobs\UpdateUserAvatar;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

final readonly class GitHub
{
Expand Down Expand Up @@ -41,6 +47,55 @@ public function isCompanySponsor(string $username): bool
)->values()->isNotEmpty();
}

/**
* Validates the user received from Github and links our user to it
*
* @return array<string, mixed> list of validation errors
*/
public function linkGitHubUser(?string $githubUsername, User $user, Request $request): array
{
try {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the code that we extracted from the UserGitHubUsernameController

$validated = $this->validateGitHubUsername($githubUsername);
} catch (ValidationException $e) {
return $e->errors();
}

$user->update($validated);

SyncVerifiedUser::dispatchSync($user);

$user = type($user->fresh())->as(User::class);

$user->is_verified
? $request->session()->flash('flash-message', 'Your GitHub account has been connected and you are now verified.')
: $request->session()->flash('flash-message', 'Your GitHub account has been connected.');

if (! $user->is_uploaded_avatar) {
UpdateUserAvatar::dispatch(
user: $user,
service: 'github',
);
}

return [];
}

/**
* @return array<string, mixed>
*
* @throws ValidationException
*/
private function validateGitHubUsername(?string $githubUsername): array
{
return Validator::validate([
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted this function because PhpStan seemed to be unable to guess that this piece of code could throw a ValidationException

'github_username' => $githubUsername,
], [
'github_username' => ['required', 'string', 'max:255', 'unique:users,github_username'],
], [
'github_username.unique' => 'This GitHub username is already connected to another account.',
]);
}

/**
* Get the content from the GitHub API.
*
Expand Down
2 changes: 1 addition & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_REDIRECT_URI'),
'redirect' => '',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now set dynamically

'token' => env('GITHUB_PERSONAL_ACCESS_TOKEN'),
],
];
16 changes: 16 additions & 0 deletions resources/views/auth/login.blade.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<x-guest-layout>
<x-input-error
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We display the errors related to the GitHub login at the top of the page

:messages="$errors->github->get('github_username')"
class="mb-5"
/>

<form
method="POST"
action="{{ route('login') }}"
Expand Down Expand Up @@ -91,6 +96,17 @@ class="text-sm dark:text-slate-200 text-slate-800 underline hover:no-underline"
</div>
</form>

<div class="mt-4 text-center">
<a
href="{{ route('profile.connect.github.login') }}"
>
<span class="inline-flex items-center gap-x-2 text-sm dark:text-slate-200 text-slate-800 underline hover:no-underline">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new "Log in using GitHub" link

<x-icons.github class="inline-block h-6 w-6 mr-100" />
{{ __('Log in using GitHub') }}
</span>
</a>
</div>

<x-section-border />

<div class="mt-4 text-center text-sm text-slate-500">
Expand Down
25 changes: 23 additions & 2 deletions resources/views/auth/register.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
></script>
@endsection

<x-input-error
:messages="$errors->github->get('github_username')"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors related to the GithHub registrfation

class="mb-5"
/>

@if ($githubUsername)
<div class="mb-5">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have received the email from GitHub, we invite the user to fill in the rest of their details

{{ __('To complete your registration using GitHub, please fill in these additional fields:') }}
</div>
@endif

<form
method="POST"
action="{{ route('register') }}"
Expand Down Expand Up @@ -61,11 +72,12 @@ class="mt-2"
/>
<x-text-input
id="email"
class="mt-1 block w-full"
class="mt-1 block w-full {{ $githubEmail !== null ? 'text-slate-500' : '' }}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we received the email from GitHub we mark it as readonly and with a lighter colour

type="email"
name="email"
:value="old('email')"
:value="old('email', $githubEmail)"
required
:readonly="$githubEmail !== null"
autocomplete="email"
/>
<x-input-error
Expand Down Expand Up @@ -162,6 +174,15 @@ class="mt-2"
/>
@endif

@if ($githubUsername)
<x-text-input
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We add a hidden field to be able to send back the GitHub username back to the registration controller

id="github_username"
name="github_username"
type="hidden"
:value="$githubUsername"
/>
@endif

<div class="mt-4 flex items-center justify-end space-x-3.5 text-sm">
<div>
<span class="text-slate-500">Already have an account?</span>
Expand Down
10 changes: 10 additions & 0 deletions routes/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\GitHubLoginController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
Expand Down Expand Up @@ -47,6 +48,15 @@
->middleware('throttle:two-factor');
});

Route::prefix('/profile/connect/github')->group(function () {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new routes for the GitHub login. They don't need authorization

Route::get('/login', [GitHubLoginController::class, 'index'])
->name('profile.connect.github.login');

Route::get('/login/callback', [
GitHubLoginController::class, 'update',
])->name('profile.connect.github.login.callback');
});

Route::middleware('auth')->group(function () {
Route::get('verify-email', EmailVerificationPromptController::class)
->name('verification.notice');
Expand Down
Loading