get_authorization_header(); return $auth_header && strpos($auth_header, 'Bearer ') === 0 && $this->looks_like_jwt($auth_header); } public function authenticate(): ?int { if (!$this->can_authenticate()) { return null; } $jwt_token = $this->extract_jwt_token(); if (!$jwt_token) { throw new \Exception('Invalid JWT token format'); } $payload = $this->validate_jwt($jwt_token); if (!$payload) { throw new \Exception('Invalid or expired JWT token'); } $user_id = $payload['sub'] ?? null; if (!$user_id) { throw new \Exception('JWT token missing user identifier'); } // Verify user still exists $user = get_user_by('ID', $user_id); if (!$user) { throw new \Exception('User account not found'); } return (int) $user_id; } public function validate_credentials(): bool { if (!$this->can_authenticate()) { return false; } $jwt_token = $this->extract_jwt_token(); return $jwt_token && $this->validate_jwt($jwt_token) !== null; } public function get_priority(): int { return 15; // Between OAuth2 and API key } public function requires_https(): bool { return true; // JWTs should require HTTPS } public function get_allowed_methods(): array { return ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; } public function get_response_headers(): array { return [ 'WWW-Authenticate' => 'Bearer realm="WordPress JWT"', ]; } /** * Get Authorization header from request */ private function get_authorization_header(): ?string { if (isset($_SERVER['HTTP_AUTHORIZATION'])) { return $_SERVER['HTTP_AUTHORIZATION']; } if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { return $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } if (function_exists('getallheaders')) { $headers = getallheaders(); if (isset($headers['Authorization'])) { return $headers['Authorization']; } } return null; } /** * Check if the token looks like a JWT (has 3 parts separated by dots) */ private function looks_like_jwt(string $auth_header): bool { $token = substr($auth_header, 7); // Remove "Bearer " prefix return substr_count($token, '.') === 2; } /** * Extract JWT token from Authorization header */ private function extract_jwt_token(): ?string { $auth_header = $this->get_authorization_header(); if (!$auth_header || strpos($auth_header, 'Bearer ') !== 0) { return null; } $token = substr($auth_header, 7); // Remove "Bearer " prefix $token = trim($token); // Basic JWT format validation (3 parts separated by dots) if (substr_count($token, '.') !== 2) { return null; } return $token; } /** * Validate JWT token and return payload * This is a simplified JWT validation for PoC - in production use a proper JWT library */ private function validate_jwt(string $jwt_token): ?array { $parts = explode('.', $jwt_token); if (count($parts) !== 3) { return null; } [$header_encoded, $payload_encoded, $signature_encoded] = $parts; // Decode header and payload $header = json_decode($this->base64url_decode($header_encoded), true); $payload = json_decode($this->base64url_decode($payload_encoded), true); if (!$header || !$payload) { return null; } // Check algorithm if (($header['alg'] ?? '') !== 'HS256') { return null; } // Verify signature $secret = $this->get_jwt_secret(); $expected_signature = $this->base64url_encode( hash_hmac('sha256', $header_encoded . '.' . $payload_encoded, $secret, true) ); if (!hash_equals($expected_signature, $signature_encoded)) { return null; } // Check expiration if (isset($payload['exp']) && time() > $payload['exp']) { return null; } // Check not before if (isset($payload['nbf']) && time() < $payload['nbf']) { return null; } // Check issued at if (isset($payload['iat']) && time() < $payload['iat']) { return null; } return $payload; } /** * Get or generate JWT secret */ private function get_jwt_secret(): string { $secret = get_option(self::JWT_SECRET_OPTION); if (!$secret) { $secret = bin2hex(random_bytes(32)); update_option(self::JWT_SECRET_OPTION, $secret); } return $secret; } /** * Base64URL decode */ private function base64url_decode(string $data): string { return base64_decode(strtr($data, '-_', '+/')); } /** * Base64URL encode */ private function base64url_encode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } /** * Generate a JWT token for a user */ public static function generate_jwt_for_user(int $user_id, int $expires_in = 3600): string { $header = [ 'typ' => 'JWT', 'alg' => 'HS256' ]; $payload = [ 'iss' => home_url(), 'sub' => (string) $user_id, 'aud' => home_url(), 'iat' => time(), 'nbf' => time(), 'exp' => time() + $expires_in, ]; $header_encoded = (new self())->base64url_encode(json_encode($header)); $payload_encoded = (new self())->base64url_encode(json_encode($payload)); $secret = (new self())->get_jwt_secret(); $signature = (new self())->base64url_encode( hash_hmac('sha256', $header_encoded . '.' . $payload_encoded, $secret, true) ); return $header_encoded . '.' . $payload_encoded . '.' . $signature; } }