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)); } }