states, and pulls data from the authenticated /api/v1/tenant/packages endpoint.
(resources/js/admin/pages/EventFormPage.tsx, resources/js/admin/api.ts)
- Harden tenant-admin auth flow: prevent PKCE state loss, scope out StrictMode double-processing, add SPA
routes for /event-admin/login and /event-admin/logout, and tighten token/session clearing semantics (resources/js/admin/auth/{context,tokens}.tsx, resources/js/admin/pages/{AuthCallbackPage,LogoutPage}.tsx,
resources/js/admin/router.tsx, routes/web.php)
114 lines
4.1 KiB
PHP
114 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
class OAuthRotateKeysCommand extends Command
|
|
{
|
|
protected $signature = 'oauth:rotate-keys {--kid=} {--force : Do not prompt for confirmation}';
|
|
|
|
protected $description = 'Generate a new JWT signing key pair for tenant OAuth tokens.';
|
|
|
|
public function handle(): int
|
|
{
|
|
$storage = rtrim(config('oauth.keys.storage_path', storage_path('app/oauth-keys')), DIRECTORY_SEPARATOR);
|
|
$currentKid = config('oauth.keys.current_kid', 'fotospiel-jwt');
|
|
$newKid = $this->option('kid') ?: 'kid-'.now()->format('YmdHis');
|
|
|
|
if (! $this->option('force') &&
|
|
! $this->confirm("Rotate JWT keys? Current kid: {$currentKid}. New kid: {$newKid}", true)
|
|
) {
|
|
$this->info('Rotation cancelled.');
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
File::ensureDirectoryExists($storage);
|
|
|
|
$archiveDir = $this->archiveExistingKeys($storage, $currentKid);
|
|
|
|
$newDirectory = $storage.DIRECTORY_SEPARATOR.$newKid;
|
|
if (File::exists($newDirectory)) {
|
|
$this->error("Target directory already exists: {$newDirectory}");
|
|
return self::FAILURE;
|
|
}
|
|
|
|
File::makeDirectory($newDirectory, 0700, true);
|
|
$this->generateKeyPair($newDirectory);
|
|
|
|
$this->info('New signing keys generated.');
|
|
$this->line("Path: {$newDirectory}");
|
|
|
|
if ($archiveDir) {
|
|
$this->line("Previous keys archived at: {$archiveDir}");
|
|
$this->line('Existing key remains available for token verification until you prune it.');
|
|
}
|
|
|
|
$this->warn("Update OAUTH_JWT_KID in your environment configuration to: {$newKid}");
|
|
$this->info('Run `php artisan oauth:list-keys` to verify active signing directories.');
|
|
$this->info('Once legacy tokens expire, run `php artisan oauth:prune-keys` to remove retired keys.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function archiveExistingKeys(string $storage, string $kid): ?string
|
|
{
|
|
$existingDir = $storage.DIRECTORY_SEPARATOR.$kid;
|
|
$legacyPublic = storage_path('app/public.key');
|
|
$legacyPrivate = storage_path('app/private.key');
|
|
|
|
if (File::exists($existingDir)) {
|
|
$archiveDir = $storage.DIRECTORY_SEPARATOR.'archive'.DIRECTORY_SEPARATOR.$kid.'-'.now()->format('YmdHis');
|
|
File::ensureDirectoryExists(dirname($archiveDir));
|
|
File::copyDirectory($existingDir, $archiveDir);
|
|
return $archiveDir;
|
|
}
|
|
|
|
if (File::exists($legacyPublic) || File::exists($legacyPrivate)) {
|
|
$archiveDir = $storage.DIRECTORY_SEPARATOR.'archive'.DIRECTORY_SEPARATOR.'legacy-'.now()->format('YmdHis');
|
|
File::ensureDirectoryExists($archiveDir);
|
|
|
|
if (File::exists($legacyPublic)) {
|
|
File::copy($legacyPublic, $archiveDir.DIRECTORY_SEPARATOR.'public.key');
|
|
}
|
|
|
|
if (File::exists($legacyPrivate)) {
|
|
File::copy($legacyPrivate, $archiveDir.DIRECTORY_SEPARATOR.'private.key');
|
|
}
|
|
|
|
return $archiveDir;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function generateKeyPair(string $directory): void
|
|
{
|
|
$config = [
|
|
'digest_alg' => OPENSSL_ALGO_SHA256,
|
|
'private_key_bits' => 4096,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
|
];
|
|
|
|
$resource = openssl_pkey_new($config);
|
|
if (! $resource) {
|
|
throw new \RuntimeException('Failed to generate key pair');
|
|
}
|
|
|
|
openssl_pkey_export($resource, $privateKey);
|
|
$details = openssl_pkey_get_details($resource);
|
|
$publicKey = $details['key'] ?? null;
|
|
|
|
if (! $publicKey) {
|
|
throw new \RuntimeException('Unable to extract public key');
|
|
}
|
|
|
|
File::put($directory.DIRECTORY_SEPARATOR.'private.key', $privateKey);
|
|
File::chmod($directory.DIRECTORY_SEPARATOR.'private.key', 0600);
|
|
|
|
File::put($directory.DIRECTORY_SEPARATOR.'public.key', $publicKey);
|
|
File::chmod($directory.DIRECTORY_SEPARATOR.'public.key', 0644);
|
|
}
|
|
}
|