diff --git a/resources/views/oauth/prompt.blade.php b/resources/views/oauth/prompt.blade.php new file mode 100644 index 00000000..371e7b28 --- /dev/null +++ b/resources/views/oauth/prompt.blade.php @@ -0,0 +1,47 @@ + + + + + + + Laravel + + + + + + + + + +
+
+

+ Confirm connection of your {{ \JoelButcher\Socialstream\Providers::name($provider) }} account. +

+ +
+ @csrf +

+ @if (config('socialstream.confirmation-prompt')) + {{ config('socialstream.confirmation-prompt') }} + @else + To ensure you are the account owner of this {{ \JoelButcher\Socialstream\Providers::name($provider) }} account, + please confirm or deny the request below to link this provider to your account. + @endif +

+ +
+ + + +
+
+
+
+ + diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 00000000..dcc6b596 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,12 @@ + config('socialstream.middleware', ['web'])], function () { + Route::get('/oauth/{provider}/callback/prompt', [OAuthController::class, 'prompt'])->name('oauth.callback.prompt'); + Route::post('/oauth/{provider}/callback/confirm', [OAuthController::class, 'confirm'])->name( + 'oauth.callback.confirm' + ); +}); diff --git a/src/Actions/AuthenticateOAuthCallback.php b/src/Actions/AuthenticateOAuthCallback.php index c874a288..f44cd0c4 100644 --- a/src/Actions/AuthenticateOAuthCallback.php +++ b/src/Actions/AuthenticateOAuthCallback.php @@ -65,7 +65,10 @@ public function authenticate(string $provider, ProviderUser $providerAccount): S { // If the user is authenticated, link the provider to the authenticated user. if ($user = auth()->user()) { - return $this->link($user, $provider, $providerAccount); + // cache the provider account for 10 mins whilst the user is redirected to the confirmation screen. + cache()->put("socialstream.{$user->id}:$provider.provider", $providerAccount, ttl: new \DateInterval('PT10M')); + + return redirect()->route('oauth.callback.prompt', $provider); } // Check if the user has an existing OAuth account. @@ -198,16 +201,11 @@ function ($request, $next) { ])); } - /** - * Attempt to link the provider to the authenticated user. - * - * Attempt to link the provider with the authenticated user. - */ - private function link(Authenticatable $user, string $provider, ProviderUser $providerAccount): SocialstreamResponse + public function link(Authenticatable $user, string $provider, ProviderUser $providerAccount): SocialstreamResponse { $account = Socialstream::findConnectedAccountForProviderAndId($provider, $providerAccount->getId()); - if ($account && $user?->id !== $account->user_id) { + if ($account && $user->id !== $account->user_id) { event(new OAuthProviderLinkFailed($user, $provider, $account, $providerAccount)); $this->flashError( diff --git a/src/Http/Controllers/OAuthController.php b/src/Http/Controllers/OAuthController.php index 17d5b9ea..8414a78f 100644 --- a/src/Http/Controllers/OAuthController.php +++ b/src/Http/Controllers/OAuthController.php @@ -2,17 +2,25 @@ namespace JoelButcher\Socialstream\Http\Controllers; +use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Routing\Controller; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Session; +use Illuminate\Support\MessageBag; +use Illuminate\Support\ViewErrorBag; use JoelButcher\Socialstream\Contracts\AuthenticatesOAuthCallback; use JoelButcher\Socialstream\Contracts\GeneratesProviderRedirect; use JoelButcher\Socialstream\Contracts\HandlesInvalidState; use JoelButcher\Socialstream\Contracts\HandlesOAuthCallbackErrors; use JoelButcher\Socialstream\Contracts\ResolvesSocialiteUsers; use JoelButcher\Socialstream\Contracts\SocialstreamResponse; +use JoelButcher\Socialstream\Events\OAuthProviderLinkFailed; +use JoelButcher\Socialstream\Http\Responses\OAuthProviderLinkFailedResponse; +use JoelButcher\Socialstream\Providers; +use Laravel\Jetstream\Jetstream; use Laravel\Socialite\Two\InvalidStateException; use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse; @@ -59,4 +67,58 @@ public function callback(Request $request, string $provider): SocialstreamRespon return $this->authenticator->authenticate($provider, $providerAccount); } + + /** + * Show the oauth confirmation page. + */ + public function prompt(string $provider): View + { + return view('socialstream::oauth.prompt', [ + 'provider' => $provider, + ]); + } + + public function confirm(string $provider): SocialstreamResponse|RedirectResponse + { + $user = auth()->user(); + $providerAccount = cache()->pull("socialstream.{$user->id}:$provider.provider"); + + $result = request()->input('result'); + + if ($result === 'deny') { + event(new OAuthProviderLinkFailed($user, $provider, null, $providerAccount)); + + $this->flashError( + __('Failed to link :provider account. User denied request.', ['provider' => Providers::name($provider)]), + ); + + return app(OAuthProviderLinkFailedResponse::class); + } + + if (!$providerAccount) { + throw new \DomainException( + message: 'Could not retrieve social provider information.' + ); + } + + return $this->authenticator->link($user, $provider, $providerAccount); + } + + private function flashError(string $error): void + { + if (auth()->check()) { + if (class_exists(Jetstream::class)) { + Session::flash('flash.banner', $error); + Session::flash('flash.bannerStyle', 'danger'); + + return; + } + } + + Session::flash('errors', (new ViewErrorBag())->put( + 'default', + new MessageBag(['socialstream' => $error]) + )); + } + } diff --git a/src/SocialstreamServiceProvider.php b/src/SocialstreamServiceProvider.php index 1ecb4826..0a9ea12c 100644 --- a/src/SocialstreamServiceProvider.php +++ b/src/SocialstreamServiceProvider.php @@ -141,6 +141,7 @@ private function configureRoutes(): void 'domain' => config('socialstream.domain'), 'prefix' => config('socialstream.prefix', config('socialstream.path')), ], function () { + $this->loadRoutesFrom(path: __DIR__.'/../routes/web.php'); $this->loadRoutesFrom(path: __DIR__.'/../routes/'.config('jetstream.stack', 'livewire').'.php'); }); }