Files
boursecu_server/app/Http/Middleware/EnsureSuperAdminRemoteToken.php
T

144 lines
4.2 KiB
PHP
Raw Normal View History

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
/**
* Auth for SuperAdmin → product (kill-switch / super settings):
* 1) Authorization Bearer matching SUPERADMIN_REMOTE_TOKEN
* OR SUPERADMIN_SMS_SHARED_SECRET (CDN-safe; same secret as SMS)
* 2) Optional: shared HMAC headers X-Abshode-* (may be stripped by CDN)
*/
class EnsureSuperAdminRemoteToken
{
private const MAX_SKEW_SECONDS = 300;
private const CANONICAL_PATH = '/api/super/settings';
public function handle(Request $request, Closure $next)
{
if ($this->authByBearer($request)) {
return $next($request);
}
if ($this->authByHmac($request)) {
return $next($request);
}
$hasSecret = $this->sharedSecret() !== '';
$hasToken = $this->bearerToken() !== '';
if (! $hasSecret && ! $hasToken) {
return response()->json([
'message' => 'SUPERADMIN_SMS_SHARED_SECRET or SUPERADMIN_REMOTE_TOKEN is not configured',
], 503);
}
return response()->json(['message' => 'Unauthorized'], 401);
}
private function authByHmac(Request $request): bool
{
$secret = $this->sharedSecret();
if ($secret === '') {
return false;
}
$domainHeader = trim((string) $request->header('X-Abshode-Domain', ''));
$tsRaw = trim((string) $request->header('X-Abshode-Timestamp', ''));
$sigRaw = trim((string) $request->header('X-Abshode-Signature', ''));
if ($domainHeader === '' || $tsRaw === '' || $sigRaw === '') {
return false;
}
if (! ctype_digit($tsRaw)) {
return false;
}
$timestamp = (int) $tsRaw;
if (abs(time() - $timestamp) > self::MAX_SKEW_SECONDS) {
return false;
}
$domain = $this->normalizeHost($domainHeader);
if ($domain === '' || ! $this->domainAllowed($domain, $request)) {
return false;
}
$method = strtoupper($request->getMethod());
$payload = $timestamp.'.'.$method.'.'.self::CANONICAL_PATH.'.'.$domain;
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals(strtolower($expected), strtolower($sigRaw));
}
private function authByBearer(Request $request): bool
{
$auth = (string) $request->header('Authorization', '');
if (! preg_match('/^\s*Bearer\s+(.+)\s*$/i', $auth, $m)) {
return false;
}
$token = trim($m[1]);
if ($token === '') {
return false;
}
foreach ([$this->bearerToken(), $this->sharedSecret()] as $expected) {
if ($expected !== '' && hash_equals($expected, $token)) {
return true;
}
}
return false;
}
private function domainAllowed(string $domain, Request $request): bool
{
$candidates = [
$this->configuredDomain(),
$this->normalizeHost((string) $request->getHost()),
];
foreach ($candidates as $candidate) {
if ($candidate !== '' && hash_equals($candidate, $domain)) {
return true;
}
}
return false;
}
private function sharedSecret(): string
{
return trim((string) config('services.superadmin.sms_shared_secret', ''));
}
private function bearerToken(): string
{
return trim((string) config('services.superadmin.remote_token', ''));
}
private function configuredDomain(): string
{
$override = trim((string) config('services.superadmin.sms_domain', ''));
if ($override !== '') {
return $this->normalizeHost($override);
}
$fromApp = parse_url((string) config('app.url'), PHP_URL_HOST);
return $this->normalizeHost(is_string($fromApp) ? $fromApp : '');
}
private function normalizeHost(string $value): string
{
$host = strtolower(trim($value));
$host = preg_replace('#^https?://#', '', $host) ?? $host;
$host = explode('/', $host)[0] ?? $host;
$host = explode(':', $host)[0] ?? $host;
return trim($host);
}
}