Offer «Entrar con Pireon» on the panel's login page
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / test-postgres (push) Has been cancelled
Tests / mcp (push) Has been cancelled

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:
2026-10-16 12:46:07 +02:00
parent 3c119d425e
commit ec4f687ca8
12 changed files with 169 additions and 14 deletions

View File

@@ -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);

View File

@@ -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();