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

[6.x] Fix authenticating existing users if the underlying users email has been changed #351

Merged
merged 6 commits into from
Mar 27, 2024
Merged
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
104 changes: 52 additions & 52 deletions src/Actions/AuthenticateOAuthCallback.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,25 @@
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\MessageBag;
use Illuminate\Support\ViewErrorBag;
use JoelButcher\Socialstream\Concerns\InteractsWithComposer;
use JoelButcher\Socialstream\Contracts\AuthenticatesOAuthCallback;
use JoelButcher\Socialstream\Contracts\CreatesConnectedAccounts;
use JoelButcher\Socialstream\Contracts\CreatesUserFromProvider;
use JoelButcher\Socialstream\Contracts\OAuthLoginFailedResponse;
use JoelButcher\Socialstream\Contracts\OAuthFailedResponse;
use JoelButcher\Socialstream\Contracts\OAuthLoginResponse;
use JoelButcher\Socialstream\Contracts\OAuthProviderLinkedResponse;
use JoelButcher\Socialstream\Contracts\OAuthProviderLinkFailedResponse;
use JoelButcher\Socialstream\Contracts\OAuthRegisterFailedResponse;
use JoelButcher\Socialstream\Contracts\OAuthRegisterResponse;
use JoelButcher\Socialstream\Contracts\SocialstreamResponse;
use JoelButcher\Socialstream\Contracts\UpdatesConnectedAccounts;
use JoelButcher\Socialstream\Events\NewOAuthRegistration;
use JoelButcher\Socialstream\Events\OAuthFailed;
use JoelButcher\Socialstream\Events\OAuthLogin;
use JoelButcher\Socialstream\Events\OAuthLoginFailed;
use JoelButcher\Socialstream\Events\OAuthProviderLinked;
use JoelButcher\Socialstream\Events\OAuthProviderLinkFailed;
use JoelButcher\Socialstream\Events\OAuthRegistrationFailed;
use JoelButcher\Socialstream\Features;
use JoelButcher\Socialstream\Providers;
use JoelButcher\Socialstream\Socialstream;
Expand Down Expand Up @@ -54,51 +53,54 @@ public function __construct(
public function authenticate(string $provider, ProviderUser $providerAccount): SocialstreamResponse|RedirectResponse
{
if ($user = auth()->user()) {
return $this->linkProvider($user, $provider, $providerAccount);
return $this->link($user, $provider, $providerAccount);
}

// The user is not authenticated, we will attempt to resolve the user
// and provider account. If we find both, and the enabled features
// allow for it, we will attempt to authenticate the user.
$account = $this->findAccount($provider, $providerAccount);
$account = Socialstream::findConnectedAccountForProviderAndId($provider, $providerAccount->getId());
$user = Socialstream::newUserModel()->where('email', $providerAccount->getEmail())->first();

if ($account && $user) {
return $this->login($user, $account, $provider, $providerAccount);
return $this->login(
user: $user,
account: $account,
provider: $provider,
providerAccount: $providerAccount
);
}

// Determine if the user can be registered and register them if so.
if (! $account && !$user && $this->canRegister()) {
if ($this->canRegister($user, $account)) {
return $this->register($provider, $providerAccount);
}

// User does not exist, return an errored response
// instructing the user to register with the app.
if (! $user) {
event(new OAuthLoginFailed($provider, $providerAccount));

$this->flashError(
__('We could not find your account. Please register to create an account.'),
if (! $user && $account && $account->user) {
return $this->login(
user: $account->user,
account: $account,
provider: $provider,
providerAccount: $providerAccount
);

return app(OAuthLoginFailedResponse::class);
}

// Account does not exist, but a user does, check to see if the features
// allow creating a new connected account for the provider
if (! $account && Features::authenticatesExistingUnlinkedUsers()) {
$account = $this->createsConnectedAccounts->create($user, $provider, $providerAccount);

return $this->login($user, $account, $provider, $providerAccount);
if ($user && Features::authenticatesExistingUnlinkedUsers()) {
return $this->login(
user: $user,
account: $this->createsConnectedAccounts->create(
user: $user,
provider: $provider,
providerUser: $providerAccount,
),
provider: $provider,
providerAccount: $providerAccount
);
}

event(new OAuthRegistrationFailed($provider, $account, $providerAccount));
event(new OAuthFailed($provider, $providerAccount));

$this->flashError(
__('An account already exists for that email address. Please login to connect your :provider account.', ['provider' => Providers::name($provider)]),
);

return app(OAuthRegisterFailedResponse::class);
return app(OAuthFailedResponse::class);
}

/**
Expand Down Expand Up @@ -132,12 +134,11 @@ protected function login(Authenticatable $user, mixed $account, string $provider
/**
* Attempt to link the provider to the authenticated user.
*
* If a connected account associated with the provider already exists,
* and is linked to another user, we will return an error.
* Attempt to link the provider with the authenticated user.
*/
private function linkProvider(Authenticatable $user, string $provider, ProviderUser $providerAccount): SocialstreamResponse
private function link(Authenticatable $user, string $provider, ProviderUser $providerAccount): SocialstreamResponse
{
$account = $this->findAccount($provider, $providerAccount);
$account = Socialstream::findConnectedAccountForProviderAndId($provider, $providerAccount->getId());

if ($account && $user?->id !== $account->user_id) {
event(new OAuthProviderLinkFailed($user, $provider, $account, $providerAccount));
Expand All @@ -162,27 +163,19 @@ private function linkProvider(Authenticatable $user, string $provider, ProviderU
return app(OAuthProviderLinkedResponse::class);
}

/**
* Find an existing connected account for the given provider and provider id.
*/
private function findAccount(string $provider, ProviderUser $providerAccount): mixed
{
return Socialstream::findConnectedAccountForProviderAndId($provider, $providerAccount->getId());
}

/**
* Flash a status message to the session.
*/
private function flashStatus(string $status): void
{
if (class_exists(Jetstream::class)) {
session()->flash('flash.banner', $status);
session()->flash('flash.bannerStyle', 'success');
Session::flash('flash.banner', $status);
Session::flash('flash.bannerStyle', 'success');

return;
}

session()->flash('status', $status);
Session::flash('status', $status);
}

/**
Expand All @@ -192,29 +185,36 @@ private function flashError(string $error): void
{
if (auth()->check()) {
if (class_exists(Jetstream::class)) {
session()->flash('flash.banner', $error);
session()->flash('flash.bannerStyle', 'danger');
Session::flash('flash.banner', $error);
Session::flash('flash.bannerStyle', 'danger');

return;
}
}

session()->flash('errors', (new ViewErrorBag())->put(
Session::flash('errors', (new ViewErrorBag())->put(
'default',
new MessageBag(['socialstream' => $error])
));
}

private function canRegister(): bool
/**
* Determine if we can register a new user.
*/
private function canRegister(mixed $user, mixed $account): bool
{
if (Route::has('register') && session()->get('socialstream.previous_url') === route('register')) {
if (! is_null($user) || !is_null($account)) {
return false;
}

if (Route::has('register') && Session::get('socialstream.previous_url') === route('register')) {
return true;
}

if (! Features::hasCreateAccountOnFirstLoginFeatures()) {
return false;
if (Session::get('socialstream.previous_url') !== route('login')) {
return Features::hasGlobalLoginFeatures();
}

return session()->get('socialstream.previous_url') === route('login') || Features::hasGlobalLoginFeatures();
return Features::hasCreateAccountOnFirstLoginFeatures();
}
}
3 changes: 2 additions & 1 deletion src/Actions/GenerateRedirectForProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace JoelButcher\Socialstream\Actions;

use Illuminate\Support\Facades\Session;
use JoelButcher\Socialstream\Contracts\GeneratesProviderRedirect;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;
Expand All @@ -13,7 +14,7 @@ class GenerateRedirectForProvider implements GeneratesProviderRedirect
*/
public function generate(string $provider): RedirectResponse
{
session()->put(
Session::put(
key: 'socialstream.previous_url',
value: url()->previous(),
);
Expand Down
5 changes: 3 additions & 2 deletions src/Actions/HandleOAuthCallbackErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\MessageBag;
use JoelButcher\Socialstream\Concerns\InteractsWithComposer;
use JoelButcher\Socialstream\Contracts\HandlesOAuthCallbackErrors;
Expand Down Expand Up @@ -32,7 +33,7 @@ public function handle(Request $request): ?RedirectResponse
return $this->unauthenticatedRedirectWithError($error);
}

$previousUrl = session()->pull('socialstream.previous_url');
$previousUrl = Session::pull('socialstream.previous_url');

return match (true) {
Route::has('filament.admin.home') && $previousUrl === route('filament.admin.home') => redirect()
Expand All @@ -56,7 +57,7 @@ public function handle(Request $request): ?RedirectResponse

private function unauthenticatedRedirectWithError(string $error): RedirectResponse
{
$previousUrl = session()->pull('socialstream.previous_url');
$previousUrl = Session::pull('socialstream.previous_url');

if (Route::has('filament.admin.auth.login') && $previousUrl === route('filament.admin.auth.login')) {
return redirect()
Expand Down
8 changes: 8 additions & 0 deletions src/Contracts/OAuthFailedResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace JoelButcher\Socialstream\Contracts;

interface OAuthFailedResponse extends SocialstreamResponse
{

}
3 changes: 3 additions & 0 deletions src/Contracts/OAuthLoginFailedResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace JoelButcher\Socialstream\Contracts;

/**
* @deprecated in v7, use OAuthFailedResponse instead.
*/
interface OAuthLoginFailedResponse extends SocialstreamResponse
{

Expand Down
3 changes: 3 additions & 0 deletions src/Contracts/OAuthRegisterFailedResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace JoelButcher\Socialstream\Contracts;

/**
* @deprecated in v7, use OAuthFailedResponse instead.
*/
interface OAuthRegisterFailedResponse extends SocialstreamResponse
{

Expand Down
23 changes: 23 additions & 0 deletions src/Events/OAuthFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace JoelButcher\Socialstream\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User as ProviderUser;

class OAuthFailed
{
use Dispatchable, InteractsWithSockets, SerializesModels;

/**
* Create a new event instance.
*/
public function __construct(
public string $provider,
public ProviderUser $providerAccount,
) {
//
}
}
3 changes: 3 additions & 0 deletions src/Events/OAuthLoginFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User as ProviderUser;

/**
* @deprecated in v7, use OAuthFailed instead.
*/
class OAuthLoginFailed
{
use Dispatchable, InteractsWithSockets, SerializesModels;
Expand Down
3 changes: 2 additions & 1 deletion src/Http/Controllers/Inertia/ConnectedAccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\ValidationException;
use Illuminate\Contracts\Auth\StatefulGuard;
use Laravel\Fortify\Actions\ConfirmPassword;
Expand Down Expand Up @@ -32,7 +33,7 @@ public function destroy(Request $request, StatefulGuard $guard, string|int $id):
->where('user_id', $request->user()->id)
->delete();

session()->flash('flash.banner', __('Account removed'));
Session::flash('flash.banner', __('Account removed'));

return back()->with('status', 'connected-account-removed');
}
Expand Down
3 changes: 2 additions & 1 deletion src/Http/Controllers/Inertia/PasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Session;
use JoelButcher\Socialstream\Contracts\SetsUserPasswords;

class PasswordController extends Controller
Expand All @@ -16,7 +17,7 @@ public function store(Request $request, SetsUserPasswords $setter): RedirectResp
{
$setter->set($request->user(), $request->only(['password', 'password_confirmation']));

session()->flash('flash.banner', __('Your password has been set.'));
Session::flash('flash.banner', __('Your password has been set.'));

return back(303);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Session;

class UpdateUserProfilePhotoController extends Controller
{
Expand All @@ -21,7 +22,7 @@ public function __invoke(Request $request): RedirectResponse
if (is_callable([$user, 'setProfilePhotoFromUrl']) && isset($account->avatar_path)) {
$user->setProfilePhotoFromUrl($account->avatar_path);

session()->flash('flash.banner', __('Profile photo updated'));
Session::flash('flash.banner', __('Profile photo updated'));
}

return back(303);
Expand Down
3 changes: 2 additions & 1 deletion src/Http/Controllers/OAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Session;
use JoelButcher\Socialstream\Contracts\AuthenticatesOAuthCallback;
use JoelButcher\Socialstream\Contracts\GeneratesProviderRedirect;
use JoelButcher\Socialstream\Contracts\HandlesInvalidState;
Expand Down Expand Up @@ -34,7 +35,7 @@ public function __construct(
*/
public function redirect(string $provider, GeneratesProviderRedirect $generator): SymfonyRedirectResponse
{
session()->put('socialstream.previous_url', back()->getTargetUrl());
Session::put('socialstream.previous_url', back()->getTargetUrl());

return $generator->generate($provider);
}
Expand Down
Loading