fix(super): accept SMS shared HMAC for kill-switch auth
Allow SUPERADMIN_SMS_SHARED_SECRET (+ domain) like SMS remote, with Bearer SUPERADMIN_REMOTE_TOKEN as fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,29 +6,120 @@ use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Bearer token must match SUPERADMIN_REMOTE_TOKEN (same as SuperAdmin Tenant.remoteApiToken).
|
||||
* Auth for SuperAdmin → product (kill-switch / super settings):
|
||||
* 1) Preferred: shared HMAC (same secret as SMS)
|
||||
* X-Abshode-Domain, X-Abshode-Timestamp, X-Abshode-Signature
|
||||
* payload = `{ts}.{METHOD}./api/super/settings.{domain}`
|
||||
* 2) Fallback: Authorization Bearer {SUPERADMIN_REMOTE_TOKEN}
|
||||
*/
|
||||
class EnsureSuperAdminRemoteToken
|
||||
{
|
||||
private const MAX_SKEW_SECONDS = 300;
|
||||
|
||||
private const CANONICAL_PATH = '/api/super/settings';
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$expected = trim((string) config('services.superadmin.remote_token', ''));
|
||||
if ($expected === '') {
|
||||
if ($this->authByHmac($request)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($this->authByBearer($request)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$hasSecret = $this->sharedSecret() !== '';
|
||||
$hasToken = $this->bearerToken() !== '';
|
||||
if (! $hasSecret && ! $hasToken) {
|
||||
return response()->json([
|
||||
'message' => 'SUPERADMIN_REMOTE_TOKEN is not configured',
|
||||
'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);
|
||||
$expectedDomain = $this->configuredDomain();
|
||||
if ($domain === '' || $expectedDomain === '' || ! hash_equals($expectedDomain, $domain)) {
|
||||
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
|
||||
{
|
||||
$expected = $this->bearerToken();
|
||||
if ($expected === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$auth = (string) $request->header('Authorization', '');
|
||||
$token = '';
|
||||
if (preg_match('/^\s*Bearer\s+(.+)\s*$/i', $auth, $m)) {
|
||||
$token = trim($m[1]);
|
||||
if (! preg_match('/^\s*Bearer\s+(.+)\s*$/i', $auth, $m)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($token === '' || ! hash_equals($expected, $token)) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
$token = trim($m[1]);
|
||||
|
||||
return $token !== '' && hash_equals($expected, $token);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user