Offer «Entrar con Pireon» on the panel's login page
Under the e-mail and password form, a button takes the redirect flow (Authorization Code + PKCE), so the password is only ever typed at the identity server. The routes for it were already there; the page had no way to reach them. A Pireon client allows one grant type and the panel's is a password client, so the redirect flow can use its own: PIREON_REDIRECT_CLIENT_ID and PIREON_REDIRECT_CLIENT_SECRET (the main client when they are empty). The button shows up once that client is set, or with PIREON_REDIRECT_LOGIN=true. Each session renews its token with the client that issued it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ use App\Services\Pireon\PireonClient;
|
||||
use App\Services\Pireon\PireonException;
|
||||
use App\Services\Pireon\TokenStore;
|
||||
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
||||
use Filament\Auth\Pages\Login;
|
||||
use Filament\Facades\Filament;
|
||||
@@ -27,6 +28,9 @@ use SensitiveParameter;
|
||||
* usable password column — but the person signing in never sees that. They
|
||||
* type their credentials here and land in the panel.
|
||||
*
|
||||
* «Entrar con Pireon», under the form, takes the redirect flow instead: the
|
||||
* password is then typed at the identity server only.
|
||||
*
|
||||
* The cost of that convenience is stated plainly because it is real: this
|
||||
* application now handles the password rather than never seeing it. It is sent
|
||||
* straight to the identity server over the password grant and is never stored,
|
||||
@@ -53,6 +57,25 @@ class PireonLogin extends Login
|
||||
return __('pireon.sign_in_intro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Under «Entrar», «Entrar con Pireon»: the redirect flow, for whoever would
|
||||
* rather not type their password here (or signs in some other way there).
|
||||
*/
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
$actions = parent::getFormActions();
|
||||
|
||||
if (config('pireon.redirect_login') && PireonClient::forRedirect()->isConfigured()) {
|
||||
$actions[] = Action::make('pireon')
|
||||
->label(__('pireon.continue_with_pireon'))
|
||||
->url(route('auth.redirect'))
|
||||
->color('gray')
|
||||
->outlined();
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filament's own flow checks the credentials against the local user table.
|
||||
* There are no local passwords here, so the whole of it is replaced: the
|
||||
@@ -110,7 +133,7 @@ class PireonLogin extends Login
|
||||
|
||||
Auth::login($user, (bool) ($data['remember'] ?? false));
|
||||
|
||||
app(TokenStore::class)->put($token);
|
||||
app(TokenStore::class)->put($token, viaRedirect: false);
|
||||
|
||||
if ($user->locale) {
|
||||
session()->put(config('locales.session_key'), $user->locale);
|
||||
|
||||
@@ -19,6 +19,8 @@ use Illuminate\Support\Facades\Log;
|
||||
*
|
||||
* The browser never sees a password belonging to this application, and the
|
||||
* authorization code alone is useless without the verifier we keep server-side.
|
||||
* It is «Entrar con Pireon» on the login page; it uses the redirect client
|
||||
* (`PireonClient::forRedirect()`), its own one when configured.
|
||||
*/
|
||||
class PireonAuthController extends Controller
|
||||
{
|
||||
@@ -38,7 +40,7 @@ class PireonAuthController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
['url' => $url, 'state' => $state, 'code_verifier' => $verifier] = $this->client->authorizationUrl();
|
||||
['url' => $url, 'state' => $state, 'code_verifier' => $verifier] = PireonClient::forRedirect()->authorizationUrl();
|
||||
} catch (PireonException $e) {
|
||||
return $this->fail($e, route('login'));
|
||||
}
|
||||
@@ -81,7 +83,7 @@ class PireonAuthController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$token = $this->client->exchangeCode((string) $request->query('code'), (string) $verifier);
|
||||
$token = PireonClient::forRedirect()->exchangeCode((string) $request->query('code'), (string) $verifier);
|
||||
$identity = $this->client->identity($token->accessToken);
|
||||
} catch (PireonException $e) {
|
||||
return $this->fail($e, route('login'));
|
||||
@@ -100,7 +102,7 @@ class PireonAuthController extends Controller
|
||||
$request->session()->regenerate();
|
||||
|
||||
Auth::login($user, remember: true);
|
||||
$this->tokens->put($token);
|
||||
$this->tokens->put($token, viaRedirect: true);
|
||||
|
||||
if ($user->locale) {
|
||||
$request->session()->put(config('locales.session_key'), $user->locale);
|
||||
|
||||
@@ -37,6 +37,22 @@ class PireonClient
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The client for the redirect flow: its own when PIREON_REDIRECT_CLIENT_ID
|
||||
* is set (a Pireon client allows one grant type), the main one otherwise.
|
||||
*/
|
||||
public static function forRedirect(): self
|
||||
{
|
||||
$own = filled(config('pireon.redirect_client_id'));
|
||||
|
||||
return new self(
|
||||
server: (string) config('pireon.server'),
|
||||
clientId: $own ? config('pireon.redirect_client_id') : config('pireon.client_id'),
|
||||
clientSecret: $own ? config('pireon.redirect_client_secret') : config('pireon.client_secret'),
|
||||
redirectUri: (string) config('pireon.redirect_uri'),
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return filled($this->clientId);
|
||||
|
||||
@@ -18,14 +18,25 @@ class TokenStore
|
||||
{
|
||||
protected const KEY = 'pireon.token';
|
||||
|
||||
/** Which client issued it: a refresh token only works with that one. */
|
||||
protected const ISSUER = 'pireon.token_issuer';
|
||||
|
||||
public function __construct(
|
||||
protected Session $session,
|
||||
protected PireonClient $client,
|
||||
) {}
|
||||
|
||||
public function put(AccessToken $token): void
|
||||
/**
|
||||
* @param bool|null $viaRedirect Whether the redirect flow's client
|
||||
* issued it; null keeps what was said.
|
||||
*/
|
||||
public function put(AccessToken $token, ?bool $viaRedirect = null): void
|
||||
{
|
||||
$this->session->put(self::KEY, $token->toArray());
|
||||
|
||||
if ($viaRedirect !== null) {
|
||||
$this->session->put(self::ISSUER, $viaRedirect ? 'redirect' : 'password');
|
||||
}
|
||||
}
|
||||
|
||||
public function get(): ?AccessToken
|
||||
@@ -37,7 +48,7 @@ class TokenStore
|
||||
|
||||
public function forget(): void
|
||||
{
|
||||
$this->session->forget(self::KEY);
|
||||
$this->session->forget([self::KEY, self::ISSUER]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +77,10 @@ class TokenStore
|
||||
}
|
||||
|
||||
try {
|
||||
$refreshed = $this->client->refresh($token->refreshToken);
|
||||
$issuer = $this->session->get(self::ISSUER) === 'redirect'
|
||||
? PireonClient::forRedirect()
|
||||
: $this->client;
|
||||
$refreshed = $issuer->refresh($token->refreshToken);
|
||||
} catch (PireonException) {
|
||||
// The refresh token was revoked or expired upstream.
|
||||
$this->forget();
|
||||
|
||||
Reference in New Issue
Block a user