162 lines
4.4 KiB
PHP
162 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Models\TenantToken;
|
|
use Closure;
|
|
use Firebase\JWT\Exceptions\TokenExpiredException;
|
|
use Firebase\JWT\JWT;
|
|
use Firebase\JWT\Key;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class TenantTokenGuard
|
|
{
|
|
/**
|
|
* Handle an incoming request.
|
|
*/
|
|
public function handle(Request $request, Closure $next, ...$scopes)
|
|
{
|
|
$token = $this->getTokenFromRequest($request);
|
|
|
|
if (!$token) {
|
|
return response()->json(['error' => 'Token not provided'], 401);
|
|
}
|
|
|
|
try {
|
|
$decoded = $this->decodeToken($token);
|
|
} catch (\Exception $e) {
|
|
return response()->json(['error' => 'Invalid token'], 401);
|
|
}
|
|
|
|
// Check token blacklist
|
|
if ($this->isTokenBlacklisted($decoded)) {
|
|
return response()->json(['error' => 'Token has been revoked'], 401);
|
|
}
|
|
|
|
// Validate scopes if specified
|
|
if (!empty($scopes) && !$this->hasScopes($decoded, $scopes)) {
|
|
return response()->json(['error' => 'Insufficient scopes'], 403);
|
|
}
|
|
|
|
// Check expiration
|
|
if ($decoded['exp'] < time()) {
|
|
// Add to blacklist on expiry
|
|
$this->blacklistToken($decoded);
|
|
return response()->json(['error' => 'Token expired'], 401);
|
|
}
|
|
|
|
// Set tenant ID on request
|
|
$request->merge(['tenant_id' => $decoded['sub']]);
|
|
$request->attributes->set('decoded_token', $decoded);
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
/**
|
|
* Get token from request (Bearer or header)
|
|
*/
|
|
private function getTokenFromRequest(Request $request): ?string
|
|
{
|
|
$header = $request->header('Authorization');
|
|
|
|
if (str_starts_with($header, 'Bearer ')) {
|
|
return substr($header, 7);
|
|
}
|
|
|
|
if ($request->header('X-API-Token')) {
|
|
return $request->header('X-API-Token');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Decode JWT token
|
|
*/
|
|
private function decodeToken(string $token): array
|
|
{
|
|
$publicKey = file_get_contents(storage_path('app/public.key'));
|
|
if (!$publicKey) {
|
|
throw new \Exception('JWT public key not found');
|
|
}
|
|
|
|
$decoded = JWT::decode($token, new Key($publicKey, 'RS256'));
|
|
return (array) $decoded;
|
|
}
|
|
|
|
/**
|
|
* Check if token is blacklisted
|
|
*/
|
|
private function isTokenBlacklisted(array $decoded): bool
|
|
{
|
|
$jti = isset($decoded['jti']) ? $decoded['jti'] : md5($decoded['sub'] . $decoded['iat']);
|
|
$cacheKey = "blacklisted_token:{$jti}";
|
|
|
|
// Check cache first (faster)
|
|
if (Cache::has($cacheKey)) {
|
|
return true;
|
|
}
|
|
|
|
// Check DB blacklist
|
|
$dbJti = $decoded['jti'] ?? null;
|
|
$blacklisted = TenantToken::where('jti', $dbJti)
|
|
->orWhere('token_hash', md5($decoded['sub'] . $decoded['iat']))
|
|
->where('expires_at', '>', now())
|
|
->exists();
|
|
|
|
if ($blacklisted) {
|
|
Cache::put($cacheKey, true, now()->addMinutes(5));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add token to blacklist
|
|
*/
|
|
private function blacklistToken(array $decoded): void
|
|
{
|
|
$jti = $decoded['jti'] ?? md5($decoded['sub'] . $decoded['iat']);
|
|
$cacheKey = "blacklisted_token:{$jti}";
|
|
|
|
// Cache for immediate effect
|
|
Cache::put($cacheKey, true, $decoded['exp'] - time());
|
|
|
|
// Store in DB for persistence
|
|
TenantToken::updateOrCreate(
|
|
[
|
|
'jti' => $jti,
|
|
'tenant_id' => $decoded['sub'],
|
|
],
|
|
[
|
|
'token_hash' => md5(json_encode($decoded)),
|
|
'ip_address' => request()->ip(),
|
|
'user_agent' => request()->userAgent(),
|
|
'expires_at' => now()->addHours(24), // Keep for 24h after expiry
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if token has required scopes
|
|
*/
|
|
private function hasScopes(array $decoded, array $requiredScopes): bool
|
|
{
|
|
$tokenScopes = $decoded['scopes'] ?? [];
|
|
|
|
if (!is_array($tokenScopes)) {
|
|
$tokenScopes = explode(' ', $tokenScopes);
|
|
}
|
|
|
|
foreach ($requiredScopes as $scope) {
|
|
if (!in_array($scope, $tokenScopes)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} |