- 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
253 lines
7.3 KiB
PHP
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;
|
|
}
|
|
} |