begin($request, $provider, 'login'); } /** Begin connecting a provider to the signed-in account. */ public function connect(Request $request, string $provider): Response { return $this->begin($request, $provider, 'link'); } public function callback(Request $request, string $provider): RedirectResponse { $case = $this->provider($provider); $intent = $request->session()->pull(self::INTENT); $expected = $request->session()->pull(self::PROVIDER); // Nobody started this exchange from here. if (! is_string($intent) || $expected !== $case->value) { return redirect()->route('login')->with('error', __('That sign-in could not be completed. Please try again.')); } $settings = SocialSettings::for($case); if (! $settings->usable()) { return redirect()->route('login')->with('error', __('That sign-in method is not available.')); } $identity = $this->gateway()->identity($settings); if ($identity === null) { return $intent === 'link' ? redirect()->route('connected-accounts.edit')->with('error', __('That sign-in could not be completed. Please try again.')) : redirect()->route('login')->with('error', __('That sign-in could not be completed. Please try again.')); } if ($intent === 'link') { return $this->completeLink($request, $settings, $identity); } $resolution = $this->authenticator->resolve($settings, $identity); if ($resolution->user === null) { return redirect()->route('login')->with('error', $resolution->refusal); } if ($resolution->linked && ! $resolution->provisioned) { $this->activity->log(Action::SocialAccountLinked, $resolution->user, $resolution->user, [ 'provider' => $case->label(), ]); } // The same account-state check a password login gets, with the // same wording. An account awaiting approval must not be let in // by a different door — including the one that just created it. $refusal = $this->signIn->refusalReason($resolution->user); if ($refusal !== null) { return redirect()->route('login')->with('error', $refusal); } // Two-factor still applies, identically: a second factor that a // provider could skip is not a second factor. if ($this->signIn->begin($resolution->user, remember: false)) { return redirect()->route('two-factor.challenge'); } $request->session()->regenerate(); return redirect()->intended($this->startPages->pathFor($resolution->user)); } private function begin(Request $request, string $provider, string $intent): Response { $case = $this->provider($provider); $settings = SocialSettings::for($case); if (! $settings->usable()) { return redirect()->route($intent === 'link' ? 'connected-accounts.edit' : 'login') ->with('error', __('That sign-in method is not available.')); } $request->session()->put([self::INTENT => $intent, self::PROVIDER => $case->value]); // Inertia::location(), not the redirect itself. Connecting starts // as an Inertia XHR from the settings screen, and an XHR follows a // 302 to the provider cross-origin, where CORS kills it before the // person ever leaves the page. The 409 + X-Inertia-Location pair // makes the client navigate top-level instead; a plain browser // request — the login flow — passes through unchanged. return Inertia::location($this->gateway()->redirect($settings)); } private function completeLink(Request $request, SocialSettings $settings, SocialIdentity $identity): RedirectResponse { $user = $request->user(); if ($user === null) { return redirect()->route('login'); } $link = $this->authenticator->link($user, $identity); if ($link === null) { return redirect()->route('connected-accounts.edit')->with( 'error', __('That :provider account is already connected to another account here.', [ 'provider' => $settings->provider->label(), ]) ); } $this->activity->log(Action::SocialAccountLinked, $user, $user, [ 'provider' => $settings->provider->label(), ]); return redirect()->route('connected-accounts.edit')->with( 'success', __(':provider connected.', ['provider' => $settings->provider->label()]) ); } private function provider(string $provider): SocialProvider { return SocialProvider::tryFrom($provider) ?? throw new NotFoundHttpException; } }