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>
96 lines
2.5 KiB
PHP
96 lines
2.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Pireon;
|
|
|
|
use Illuminate\Contracts\Session\Session;
|
|
|
|
/**
|
|
* Keeps the signed-in panel user's token set in the session and transparently
|
|
* refreshes it when it ages out.
|
|
*
|
|
* Tokens live in the session rather than the database on purpose: they are
|
|
* per-session credentials, and logging out or the session expiring should take
|
|
* them with it.
|
|
*/
|
|
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,
|
|
) {}
|
|
|
|
/**
|
|
* @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
|
|
{
|
|
$state = $this->session->get(self::KEY);
|
|
|
|
return is_array($state) ? AccessToken::fromArray($state) : null;
|
|
}
|
|
|
|
public function forget(): void
|
|
{
|
|
$this->session->forget([self::KEY, self::ISSUER]);
|
|
}
|
|
|
|
/**
|
|
* The current access token, refreshed first if it has expired.
|
|
*
|
|
* Returns null when there is no token, or when the refresh failed — the
|
|
* caller should treat that as "this session is no longer authenticated"
|
|
* and send the user back through the login flow.
|
|
*/
|
|
public function accessToken(): ?string
|
|
{
|
|
$token = $this->get();
|
|
|
|
if ($token === null) {
|
|
return null;
|
|
}
|
|
|
|
if (! $token->hasExpired()) {
|
|
return $token->accessToken;
|
|
}
|
|
|
|
if (! $token->isRefreshable()) {
|
|
$this->forget();
|
|
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$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();
|
|
|
|
return null;
|
|
}
|
|
|
|
$this->put($refreshed);
|
|
|
|
return $refreshed->accessToken;
|
|
}
|
|
}
|