Refactoring, regression testing until Phase 1 end.
This commit is contained in:
283
backend/app/Services/JwtService.php
Normal file
283
backend/app/Services/JwtService.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* JWT Service
|
||||
*
|
||||
* Handles JWT token generation, validation, and refresh token management.
|
||||
*/
|
||||
class JwtService
|
||||
{
|
||||
/**
|
||||
* Access token TTL in seconds (60 minutes)
|
||||
*/
|
||||
private const ACCESS_TOKEN_TTL = 3600;
|
||||
|
||||
/**
|
||||
* Refresh token TTL in seconds (7 days)
|
||||
*/
|
||||
private const REFRESH_TOKEN_TTL = 604800;
|
||||
|
||||
/**
|
||||
* Generate a new access token for a user
|
||||
*
|
||||
* @param User $user
|
||||
* @return string
|
||||
*/
|
||||
public function generateAccessToken(User $user): string
|
||||
{
|
||||
$payload = [
|
||||
'iss' => config('app.url', 'headroom'),
|
||||
'sub' => $user->id,
|
||||
'iat' => time(),
|
||||
'exp' => time() + self::ACCESS_TOKEN_TTL,
|
||||
'role' => $user->role,
|
||||
'permissions' => $this->getPermissions($user->role),
|
||||
'jti' => $this->generateTokenId(),
|
||||
];
|
||||
|
||||
return $this->encodeJWT($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new refresh token for a user
|
||||
*
|
||||
* @param User $user
|
||||
* @return string
|
||||
*/
|
||||
public function generateRefreshToken(User $user): string
|
||||
{
|
||||
$token = $this->generateSecureToken();
|
||||
$key = $this->getRefreshTokenKey($token);
|
||||
|
||||
Cache::put($key, $user->id, self::REFRESH_TOKEN_TTL);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user ID from a refresh token
|
||||
*
|
||||
* @param string $token
|
||||
* @return string|null
|
||||
*/
|
||||
public function getUserIdFromRefreshToken(string $token): ?string
|
||||
{
|
||||
return Cache::get($this->getRefreshTokenKey($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a refresh token
|
||||
*
|
||||
* @param string $token
|
||||
* @param string|null $userId
|
||||
* @return void
|
||||
*/
|
||||
public function invalidateRefreshToken(string $token, ?string $userId = null): void
|
||||
{
|
||||
Cache::forget($this->getRefreshTokenKey($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and decode a JWT token
|
||||
*
|
||||
* @param string $token
|
||||
* @return array|null Returns payload array or null if invalid
|
||||
*/
|
||||
public function validateToken(string $token): ?array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
|
||||
if (count($parts) !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$header, $payload, $signature] = $parts;
|
||||
|
||||
// Verify signature
|
||||
$expectedSignature = $this->createSignature($header, $payload);
|
||||
|
||||
if (! hash_equals($expectedSignature, $signature)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
$payloadData = $this->base64UrlDecode($payload);
|
||||
$payloadArray = json_decode($payloadData, true);
|
||||
|
||||
if (! is_array($payloadArray)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (isset($payloadArray['exp']) && $payloadArray['exp'] < time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $payloadArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract claims from a JWT token
|
||||
*
|
||||
* @param string $token
|
||||
* @return array|null
|
||||
*/
|
||||
public function extractClaims(string $token): ?array
|
||||
{
|
||||
return $this->validateToken($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token expiration time
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getAccessTokenTTL(): int
|
||||
{
|
||||
return self::ACCESS_TOKEN_TTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refresh token expiration time
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRefreshTokenTTL(): int
|
||||
{
|
||||
return self::REFRESH_TOKEN_TTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions for a role
|
||||
*
|
||||
* @param string $role
|
||||
* @return array
|
||||
*/
|
||||
public function getPermissions(string $role): array
|
||||
{
|
||||
return match ($role) {
|
||||
'superuser' => [
|
||||
'manage_users',
|
||||
'manage_team_members',
|
||||
'manage_projects',
|
||||
'manage_allocations',
|
||||
'manage_actuals',
|
||||
'view_reports',
|
||||
'configure_system',
|
||||
'view_audit_logs',
|
||||
],
|
||||
'manager' => [
|
||||
'manage_projects',
|
||||
'manage_allocations',
|
||||
'manage_actuals',
|
||||
'view_reports',
|
||||
'manage_team_members',
|
||||
],
|
||||
'developer' => [
|
||||
'manage_actuals',
|
||||
'view_own_allocations',
|
||||
'view_own_actuals',
|
||||
'log_hours',
|
||||
],
|
||||
'top_brass' => [
|
||||
'view_reports',
|
||||
'view_allocations',
|
||||
'view_actuals',
|
||||
'view_capacity',
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a JWT token
|
||||
*
|
||||
* @param array $payload
|
||||
* @return string
|
||||
*/
|
||||
private function encodeJWT(array $payload): string
|
||||
{
|
||||
$header = $this->base64UrlEncode(json_encode(['typ' => 'JWT', 'alg' => 'HS256']));
|
||||
$payload = $this->base64UrlEncode(json_encode($payload));
|
||||
$signature = $this->createSignature($header, $payload);
|
||||
|
||||
return $header . '.' . $payload . '.' . $signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a signature for JWT
|
||||
*
|
||||
* @param string $header
|
||||
* @param string $payload
|
||||
* @return string
|
||||
*/
|
||||
private function createSignature(string $header, string $payload): string
|
||||
{
|
||||
$signature = hash_hmac('sha256', $header . '.' . $payload, config('app.key'), true);
|
||||
|
||||
return $this->base64UrlEncode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure random token
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generateSecureToken(): string
|
||||
{
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique token ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generateTokenId(): string
|
||||
{
|
||||
return uniqid('token_', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for refresh token
|
||||
*
|
||||
* @param string $token
|
||||
* @return string
|
||||
*/
|
||||
private function getRefreshTokenKey(string $token): string
|
||||
{
|
||||
return "refresh_token:{$token}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64URL encode
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
private function base64UrlEncode(string $data): string
|
||||
{
|
||||
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64URL decode
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
private function base64UrlDecode(string $data): string
|
||||
{
|
||||
$padding = 4 - (strlen($data) % 4);
|
||||
if ($padding !== 4) {
|
||||
$data .= str_repeat('=', $padding);
|
||||
}
|
||||
|
||||
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user