144 lines
4.2 KiB
PHP
144 lines
4.2 KiB
PHP
<?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);
|
|
}
|
|
}
|