- Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env
hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads, attach packages, and surface localized success/error states. - Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/ PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent creation, webhooks, and the wizard CTA. - Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/ useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages, Checkout) with localized copy and experiment tracking. - Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations. - Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke test for the hero CTA while reconciling outstanding checklist items.
This commit is contained in:
@@ -10,6 +10,7 @@ use App\Models\TenantToken;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -22,7 +23,7 @@ class OAuthController extends Controller
|
||||
private const AUTH_CODE_TTL_MINUTES = 5;
|
||||
private const ACCESS_TOKEN_TTL_SECONDS = 3600;
|
||||
private const REFRESH_TOKEN_TTL_DAYS = 30;
|
||||
private const TOKEN_HEADER_KID = 'fotospiel-jwt';
|
||||
private const LEGACY_TOKEN_HEADER_KID = 'fotospiel-jwt';
|
||||
|
||||
/**
|
||||
* Authorize endpoint - PKCE flow
|
||||
@@ -286,9 +287,16 @@ class OAuthController extends Controller
|
||||
$storedIp = (string) ($storedRefreshToken->ip_address ?? '');
|
||||
$currentIp = (string) ($request->ip() ?? '');
|
||||
|
||||
if ($storedIp !== '' && $currentIp !== '' && ! hash_equals($storedIp, $currentIp)) {
|
||||
if (config('oauth.refresh_tokens.enforce_ip_binding', true) && ! $this->ipMatches($storedIp, $currentIp)) {
|
||||
$storedRefreshToken->update(['revoked_at' => now()]);
|
||||
|
||||
Log::warning('[OAuth] Refresh token rejected due to IP mismatch', [
|
||||
'client_id' => $request->client_id,
|
||||
'refresh_token_id' => $storedRefreshToken->id,
|
||||
'stored_ip' => $storedIp,
|
||||
'current_ip' => $currentIp,
|
||||
]);
|
||||
|
||||
return $this->errorResponse('Refresh token cannot be used from this IP address', 403);
|
||||
}
|
||||
|
||||
@@ -387,7 +395,7 @@ class OAuthController extends Controller
|
||||
int $issuedAt,
|
||||
int $expiresAt
|
||||
): string {
|
||||
[$publicKey, $privateKey] = $this->ensureKeysExist();
|
||||
[$kid, , $privateKey] = $this->getSigningKeyPair();
|
||||
|
||||
$payload = [
|
||||
'iss' => url('/'),
|
||||
@@ -403,47 +411,94 @@ class OAuthController extends Controller
|
||||
'jti' => $jti,
|
||||
];
|
||||
|
||||
return JWT::encode($payload, $privateKey, 'RS256', self::TOKEN_HEADER_KID, ['kid' => self::TOKEN_HEADER_KID]);
|
||||
return JWT::encode($payload, $privateKey, 'RS256', $kid, ['kid' => $kid]);
|
||||
}
|
||||
|
||||
private function ensureKeysExist(): array
|
||||
private function getSigningKeyPair(): array
|
||||
{
|
||||
$publicKeyPath = storage_path('app/public.key');
|
||||
$privateKeyPath = storage_path('app/private.key');
|
||||
$kid = $this->currentKid();
|
||||
[$publicKey, $privateKey] = $this->ensureKeysForKid($kid);
|
||||
|
||||
$publicKey = @file_get_contents($publicKeyPath);
|
||||
$privateKey = @file_get_contents($privateKeyPath);
|
||||
return [$kid, $publicKey, $privateKey];
|
||||
}
|
||||
|
||||
if ($publicKey && $privateKey) {
|
||||
return [$publicKey, $privateKey];
|
||||
private function currentKid(): string
|
||||
{
|
||||
return config('oauth.keys.current_kid', self::LEGACY_TOKEN_HEADER_KID);
|
||||
}
|
||||
|
||||
private function ensureKeysForKid(string $kid): array
|
||||
{
|
||||
$paths = $this->keyPaths($kid);
|
||||
|
||||
if (! File::exists($paths['directory'])) {
|
||||
File::makeDirectory($paths['directory'], 0700, true);
|
||||
}
|
||||
|
||||
$this->generateKeyPair();
|
||||
$this->maybeMigrateLegacyKeys($paths);
|
||||
|
||||
if (! File::exists($paths['public']) || ! File::exists($paths['private'])) {
|
||||
$this->generateKeyPair($paths['directory']);
|
||||
}
|
||||
|
||||
return [
|
||||
file_get_contents($publicKeyPath),
|
||||
file_get_contents($privateKeyPath),
|
||||
File::get($paths['public']),
|
||||
File::get($paths['private']),
|
||||
];
|
||||
}
|
||||
|
||||
private function generateKeyPair(): void
|
||||
private function keyPaths(string $kid): array
|
||||
{
|
||||
$base = rtrim(config('oauth.keys.storage_path', storage_path('app/oauth-keys')), DIRECTORY_SEPARATOR);
|
||||
$directory = $base.DIRECTORY_SEPARATOR.$kid;
|
||||
|
||||
return [
|
||||
'directory' => $directory,
|
||||
'public' => $directory.DIRECTORY_SEPARATOR.'public.key',
|
||||
'private' => $directory.DIRECTORY_SEPARATOR.'private.key',
|
||||
];
|
||||
}
|
||||
|
||||
private function maybeMigrateLegacyKeys(array $paths): void
|
||||
{
|
||||
$legacyPublic = storage_path('app/public.key');
|
||||
$legacyPrivate = storage_path('app/private.key');
|
||||
|
||||
if (! File::exists($paths['public']) && File::exists($legacyPublic)) {
|
||||
File::copy($legacyPublic, $paths['public']);
|
||||
}
|
||||
|
||||
if (! File::exists($paths['private']) && File::exists($legacyPrivate)) {
|
||||
File::copy($legacyPrivate, $paths['private']);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateKeyPair(string $directory): void
|
||||
{
|
||||
$config = [
|
||||
'digest_alg' => OPENSSL_ALGO_SHA256,
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_bits' => 4096,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
];
|
||||
|
||||
$res = openssl_pkey_new($config);
|
||||
if (! $res) {
|
||||
$resource = openssl_pkey_new($config);
|
||||
if (! $resource) {
|
||||
throw new \RuntimeException('Failed to generate key pair');
|
||||
}
|
||||
|
||||
openssl_pkey_export($res, $privKey);
|
||||
$pubKey = openssl_pkey_get_details($res);
|
||||
openssl_pkey_export($resource, $privateKey);
|
||||
$details = openssl_pkey_get_details($resource);
|
||||
$publicKey = $details['key'] ?? null;
|
||||
|
||||
file_put_contents(storage_path('app/private.key'), $privKey);
|
||||
file_put_contents(storage_path('app/public.key'), $pubKey['key']);
|
||||
if (! $publicKey) {
|
||||
throw new \RuntimeException('Failed to extract public key');
|
||||
}
|
||||
|
||||
File::put($directory.DIRECTORY_SEPARATOR.'private.key', $privateKey, true);
|
||||
File::chmod($directory.DIRECTORY_SEPARATOR.'private.key', 0600);
|
||||
|
||||
File::put($directory.DIRECTORY_SEPARATOR.'public.key', $publicKey, true);
|
||||
File::chmod($directory.DIRECTORY_SEPARATOR.'public.key', 0644);
|
||||
}
|
||||
private function scopesAreAllowed(array $requestedScopes, array $availableScopes): bool
|
||||
{
|
||||
@@ -480,6 +535,32 @@ class OAuthController extends Controller
|
||||
return response()->json($response, $status);
|
||||
}
|
||||
|
||||
private function ipMatches(string $storedIp, string $currentIp): bool
|
||||
{
|
||||
if ($storedIp === '' || $currentIp === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hash_equals($storedIp, $currentIp)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! config('oauth.refresh_tokens.allow_subnet_match', false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter_var($storedIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($currentIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$storedParts = explode('.', $storedIp);
|
||||
$currentParts = explode('.', $currentIp);
|
||||
|
||||
return $storedParts[0] === $currentParts[0]
|
||||
&& $storedParts[1] === $currentParts[1]
|
||||
&& $storedParts[2] === $currentParts[2];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function base64urlEncode(string $data): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
|
||||
Reference in New Issue
Block a user