Files
fotospiel-app/app/Console/Commands/OAuthRotateKeysCommand.php
Codex Agent 6290a3a448 Fix tenant event form package selector so it no longer renders empty-value options, handles loading/empty
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)
2025-10-19 23:00:47 +02:00

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