tigerstyle-scent/Auth/JwtAuthenticator.php
Ryan Malloy 1da0acd25a Initial commit: WordPress OAuth2 Server with PSR-4 architecture
- Implements complete OAuth2 authorization server for WordPress
- PSR-4 autoloading with WPOAuth2Server namespace structure
- Modular architecture with Auth, Client, Core, Storage components
- Successfully tested authorization code flow with bearer authentication
- Clean separation from WordPress plugin code for reusability
2025-09-16 20:53:00 -06:00

253 lines
7.3 KiB
PHP

<?php
/**
* JWT Token Authenticator
* Implements JWT (JSON Web Token) authentication for WordPress
*
* @package WordPress Authentication Framework PoC
*/
namespace WPOAuth2Server\Auth;
defined('ABSPATH') or die('Direct access forbidden.');
class JwtAuthenticator implements AuthenticatorInterface {
/**
* JWT secret option name
*/
private const JWT_SECRET_OPTION = 'wp_auth_framework_jwt_secret';
public function get_name(): string {
return 'jwt';
}
public function get_display_name(): string {
return __('JWT Token Authentication', 'wp-oauth2-poc');
}
public function get_description(): string {
return __('Authenticates users using JSON Web Tokens (JWT) in Authorization header', 'wp-oauth2-poc');
}
public function can_authenticate(): bool {
$auth_header = $this->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;
}
}