rateLimit(5); } catch (TooManyRequestsException $exception) { $this->getRateLimitedNotification($exception)?->send(); return null; } $data = $this->form->getState(); $this->assertUsable(); $token = $this->exchange((string) $data['email'], (string) $data['password']); $client = app(PireonClient::class); try { $identity = $client->identity($token->accessToken); } catch (PireonException $e) { $this->fail($e); } // Said only once the credentials were right, so this cannot be used to // find out which addresses have an account. if (! $identity->isUsable()) { throw ValidationException::withMessages([ 'data.email' => $identity->isApproved() ? __('pireon.inactive') : __('pireon.pending_approval'), ]); } $user = app(SyncPireonUser::class)->handle($identity); // Being a valid account upstream is not the same as being allowed in // here: roles are this application's business. if (! $user->canAccessPanel(Filament::getCurrentOrDefaultPanel())) { throw ValidationException::withMessages([ 'data.email' => __('pireon.no_permission'), ]); } // Rotate the session id so a fixated pre-login session cannot be // reused now that it carries an authenticated identity. session()->regenerate(); Auth::login($user, (bool) ($data['remember'] ?? false)); app(TokenStore::class)->put($token); if ($user->locale) { session()->put(config('locales.session_key'), $user->locale); } return app(LoginResponse::class); } /** * Trade the credentials for a token upstream. * * Any refusal answers with the same message. The identity server * distinguishes an unknown address from a wrong password; repeating that * here would turn the form into a way of finding out who has an account. */ protected function exchange(string $email, #[SensitiveParameter] string $password): AccessToken { try { return app(PireonClient::class)->requestPasswordToken($email, $password); } catch (PireonException $e) { $this->fail($e); } } /** * A misconfiguration is an operator's problem, not a visitor's, so it says * what is actually wrong instead of "those credentials are incorrect". */ protected function assertUsable(): void { if (! app(PireonClient::class)->isConfigured()) { throw ValidationException::withMessages([ 'data.email' => __('pireon.not_configured_body', ['server' => (string) config('pireon.server')]), ]); } if (! config('pireon.password_grant_enabled')) { throw ValidationException::withMessages([ 'data.email' => __('pireon.password_grant_disabled'), ]); } } protected function fail(PireonException $e): never { // The address is logged, the password never is — it does not leave the // request it arrived in. Log::warning('Pireon sign-in failed', [ 'message' => $e->getMessage(), 'status' => $e->status, ]); // Only say "those credentials are wrong" when that is what happened. // A server that answered 500, or did not answer at all, has said // nothing about the password, and reporting otherwise sends whoever is // signing in to look for a problem that is not theirs. if (! $e->isRejection()) { throw ValidationException::withMessages([ 'data.email' => __('pireon.server_unavailable'), ]); } $this->throwFailureValidationException(); } }