Skip to content

Commit

Permalink
feat: add external login providers (#1174)
Browse files Browse the repository at this point in the history
  • Loading branch information
asbiin authored Jul 12, 2021
1 parent 893ae4b commit e92d516
Show file tree
Hide file tree
Showing 33 changed files with 1,545 additions and 12 deletions.
38 changes: 38 additions & 0 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Http\Controllers\Auth;

use Inertia\Inertia;
use Inertia\Response;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Route;

class LoginController extends Controller
{
/**
* Display the login view.
*
* @param Request $request
* @return \Inertia\Response
*/
public function __invoke(Request $request): Response
{
/** @var Collection $providers */
$providers = config('auth.login_providers');
$providersName = [];
foreach ($providers as $provider) {
if ($name = config("services.$provider.name")) {
$providersName[$provider] = $name;
}
}

return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
'providers' => $providers,
'providersName' => $providersName,
]);
}
}
179 changes: 179 additions & 0 deletions app/Http/Controllers/Auth/SocialiteCallbackController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

namespace App\Http\Controllers\Auth;

use Carbon\Carbon;
use App\Models\User\User;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Models\User\UserToken;
use App\Http\Controllers\Controller;
use App\Services\User\CreateAccount;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Redirect;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\One\User as OAuth1User;
use Laravel\Socialite\Two\User as OAuth2User;
use Illuminate\Validation\ValidationException;
use Laravel\Socialite\Contracts\User as SocialiteUser;
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;

class SocialiteCallbackController extends Controller
{
/**
* Handle socalite login.
*
* @param Request $request
* @param string $driver
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function login(Request $request, string $driver): SymfonyRedirectResponse
{
$this->checkProvider($driver);

$redirect = $request->input('redirect');
if ($redirect && Str::of($redirect)->startsWith($request->getSchemeAndHttpHost())) {
Redirect::setIntendedUrl($redirect);
}

return Socialite::driver($driver)->redirect();
}

/**
* Handle socalite callback.
*
* @param Request $request
* @param string $driver
*
* @return \Illuminate\Http\RedirectResponse
*/
public function callback(Request $request, string $driver): RedirectResponse
{
try {
if ($request->input('error') != '') {
throw ValidationException::withMessages([
$request->input('error_description'),
]);
}

$this->checkProvider($driver);

$user = $this->authenticateUser($driver, Socialite::driver($driver)->user());

Auth::login($user, true);

return Redirect::intended(route('home'));
} catch (ValidationException $e) {
throw $e->redirectTo(Redirect::intended(route('default'))->getTargetUrl());
}
}

/**
* Authenticate the user.
*
* @param string $driver
* @param \Laravel\Socialite\Contracts\User $socialite
*
* @return User
*/
private function authenticateUser(string $driver, $socialite): User
{
if ($userToken = UserToken::where([
'driver_id' => $driverId = $socialite->getId(),
'driver' => $driver,
])->first()) {
// Association already exist

$user = $userToken->user;

if (($userId = Auth::id()) && $userId !== $user->id) {
throw ValidationException::withMessages([
trans('auth.provider_already_used'),
]);
}
} else {
// New association: create user or add token to existing user
$user = tap($this->getUser($socialite), function ($user) use ($driver, $driverId, $socialite) {
$this->createUserToken($user, $driver, $driverId, $socialite);
});
}

return $user;
}

/**
* Get authenticated user.
*
* @param SocialiteUser $socialite
* @return User
*/
private function getUser(SocialiteUser $socialite): User
{
if ($user = Auth::user()) {
return $user;
}

// User doesn't exist
$name = Str::of($socialite->getName())->split('/[\s]+/', 2);
$data = [
'email' => $socialite->getEmail(),
'first_name' => count($name) > 0 ? $name[0] : '',
'last_name' => count($name) > 1 ? $name[1] : '',
'nickname' => $socialite->getNickname(),
];

$user = app(CreateAccount::class)->execute($data);

$user->email_verified_at = Carbon::now();
$user->save();

return $user;
}

/**
* Create the user token register.
*
* @param User $user
* @param string $driver
* @param string $driverId
* @param SocialiteUser $socialite
* @return UserToken
*/
private function createUserToken(User $user, string $driver, string $driverId, SocialiteUser $socialite): UserToken
{
$token = [
'driver' => $driver,
'driver_id' => $driverId,
'user_id' => $user->id,
'email' => $socialite->getEmail(),
];
if ($socialite instanceof OAuth1User) {
$token['token'] = $socialite->token;
$token['token_secret'] = $socialite->tokenSecret;
$token['format'] = 'oauth1';
} elseif ($socialite instanceof OAuth2User) {
$token['token'] = $socialite->token;
$token['refresh_token'] = $socialite->refreshToken;
$token['expires_in'] = $socialite->expiresIn;
$token['format'] = 'oauth2';
} else {
throw new \Exception('authentication format not supported');
}

return UserToken::create($token);
}

/**
* Check if the driver is activated.
*
* @param string $driver
*/
private function checkProvider(string $driver): void
{
if (! collect(config('auth.login_providers'))->contains($driver)) {
throw ValidationException::withMessages(['This provider does not exist']);
}
}
}
5 changes: 2 additions & 3 deletions app/Http/Middleware/ShareInertiaData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Inertia\Inertia;
use App\Helpers\LocaleHelper;
use Illuminate\Support\Facades\Session;

/**
* Used by Jetstream to share data.
Expand Down Expand Up @@ -39,8 +38,8 @@ public function handle($request, $next)
'two_factor_enabled' => ! is_null($request->user()->two_factor_secret),
];
},
'errorBags' => function () {
return collect(optional(Session::get('errors'))->getBags() ?: [])->mapWithKeys(function ($bag, $key) {
'errorBags' => function () use ($request) {
return collect(optional($request->session()->get('errors'))->getBags() ?: [])->mapWithKeys(function ($bag, $key) {
return [$key => $bag->messages()];
})->all();
},
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Middleware/VerifyCsrfToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
protected $except = [
//
'auth/saml2/callback',
];
}
10 changes: 10 additions & 0 deletions app/Models/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ public function employees()
return $this->hasMany(Employee::class);
}

/**
* Get the user tokens for external login providers.
*
* @return HasMany
*/
public function tokens()
{
return $this->hasMany(UserToken::class);
}

/**
* Get the name of the user.
*
Expand Down
50 changes: 50 additions & 0 deletions app/Models/User/UserToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Models\User;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class UserToken extends Model
{
use HasFactory;

/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'user_id',
'driver',
'driver_id',
'email',
'format',
'token',
'token_secret',
'refresh_token',
'expires_in',
];

/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'token',
'token_secret',
'refresh_token',
];

/**
* Get the user record associated with the company.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
19 changes: 19 additions & 0 deletions app/Providers/Auth/MonicaExtendSocialite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Providers\Auth;

use SocialiteProviders\LaravelPassport\Provider;
use SocialiteProviders\Manager\SocialiteWasCalled;

class MonicaExtendSocialite
{
/**
* Register the provider.
*
* @param \SocialiteProviders\Manager\SocialiteWasCalled $socialiteWasCalled
*/
public function handle(SocialiteWasCalled $socialiteWasCalled)
{
$socialiteWasCalled->extendSocialite('monica', Provider::class);
}
}
12 changes: 11 additions & 1 deletion app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace App\Providers;

use App\Events\FileDeleted;
use Illuminate\Support\Facades\Event;
use App\Listeners\DeleteFileInStorage;
use Illuminate\Auth\Events\Registered;
use SocialiteProviders\Manager\SocialiteWasCalled;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

Expand All @@ -23,6 +23,16 @@ class EventServiceProvider extends ServiceProvider
FileDeleted::class => [
DeleteFileInStorage::class,
],
SocialiteWasCalled::class => [
'SocialiteProviders\\Azure\\AzureExtendSocialite@handle',
'SocialiteProviders\\GitHub\\GitHubExtendSocialite@handle',
'SocialiteProviders\\Google\\GoogleExtendSocialite@handle',
'App\\Providers\\Auth\\MonicaExtendSocialite@handle',
'SocialiteProviders\\LinkedIn\\LinkedInExtendSocialite@handle',
'SocialiteProviders\\Slack\\SlackExtendSocialite@handle',
'SocialiteProviders\\Saml2\\Saml2ExtendSocialite@handle',
'SocialiteProviders\\Twitter\\TwitterExtendSocialite@handle',
],
];

/**
Expand Down
5 changes: 5 additions & 0 deletions app/Providers/FortifyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Cache\RateLimiting\Limit;
use App\Services\User\UpdateUserPassword;
use Illuminate\Support\Facades\RateLimiter;
use App\Http\Controllers\Auth\LoginController;
use App\Services\User\UpdateUserProfileInformation;

class FortifyServiceProvider extends ServiceProvider
Expand All @@ -27,6 +28,10 @@ public function register()
*/
public function boot()
{
Fortify::loginView(function ($request) {
return app()->call(LoginController::class, ['request' => $request]);
});

Fortify::createUsersUsing(CreateAccount::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Expand Down
4 changes: 3 additions & 1 deletion app/Services/User/CreateAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ private function createUser(array $data): void
$locale = LocaleHelper::getLocale();
}

$password = $this->valueOrNull($data, 'password');

$this->user = User::create([
'email' => $data['email'],
'password' => Hash::make($data['password']),
'password' => $password != null ? Hash::make($password) : null,
'first_name' => $this->valueOrNull($data, 'first_name'),
'last_name' => $this->valueOrNull($data, 'last_name'),
'middle_name' => $this->valueOrNull($data, 'middle_name'),
Expand Down
Loading

0 comments on commit e92d516

Please sign in to comment.