- 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
259 lines
7.8 KiB
PHP
259 lines
7.8 KiB
PHP
<?php
|
|
/**
|
|
* OAuth2 Bearer Token Authenticator
|
|
* Implements OAuth2 Bearer token authentication for WordPress
|
|
*
|
|
* @package WordPress Authentication Framework PoC
|
|
*/
|
|
|
|
namespace WPOAuth2Server\Auth;
|
|
|
|
use WPOAuth2Server\Core\OAuth2Server;
|
|
|
|
defined('ABSPATH') or die('Direct access forbidden.');
|
|
|
|
class OAuth2BearerAuthenticator implements AuthenticatorInterface {
|
|
|
|
/**
|
|
* Token storage option name
|
|
*/
|
|
private const TOKEN_STORAGE_OPTION = 'wp_oauth2_poc_tokens';
|
|
|
|
public function get_name(): string {
|
|
return 'oauth2_bearer';
|
|
}
|
|
|
|
public function get_display_name(): string {
|
|
return __('OAuth2 Bearer Token', 'wp-oauth2-poc');
|
|
}
|
|
|
|
public function get_description(): string {
|
|
return __('Authenticates users using OAuth2 Bearer tokens in the Authorization header', 'wp-oauth2-poc');
|
|
}
|
|
|
|
public function can_authenticate(): bool {
|
|
// Check if Authorization header is present
|
|
$auth_header = $this->get_authorization_header();
|
|
|
|
// Must be Bearer token format
|
|
return $auth_header && strpos($auth_header, 'Bearer ') === 0;
|
|
}
|
|
|
|
public function authenticate(): ?int {
|
|
if (!$this->can_authenticate()) {
|
|
return null;
|
|
}
|
|
|
|
$token = $this->extract_bearer_token();
|
|
if (!$token) {
|
|
throw new \Exception('Invalid Bearer token format');
|
|
}
|
|
|
|
// Validate token and get associated user
|
|
$user_id = $this->validate_token($token);
|
|
if (!$user_id) {
|
|
throw new \Exception('Invalid or expired token');
|
|
}
|
|
|
|
// Verify user still exists and is active
|
|
$user = get_user_by('ID', $user_id);
|
|
if (!$user || !$this->is_user_active($user)) {
|
|
throw new \Exception('User account is inactive or deleted');
|
|
}
|
|
|
|
return (int) $user_id;
|
|
}
|
|
|
|
public function validate_credentials(): bool {
|
|
if (!$this->can_authenticate()) {
|
|
return false;
|
|
}
|
|
|
|
$token = $this->extract_bearer_token();
|
|
return $token && $this->validate_token($token) !== null;
|
|
}
|
|
|
|
public function get_priority(): int {
|
|
return 10; // Standard priority for OAuth2
|
|
}
|
|
|
|
public function requires_https(): bool {
|
|
return true; // OAuth2 Bearer tokens should always require HTTPS in production
|
|
}
|
|
|
|
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 REST API"',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get Authorization header from request
|
|
*/
|
|
private function get_authorization_header(): ?string {
|
|
// Try Apache/Nginx style
|
|
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
|
return $_SERVER['HTTP_AUTHORIZATION'];
|
|
}
|
|
|
|
// Try alternative header names
|
|
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
|
|
return $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
|
|
}
|
|
|
|
// Try getallheaders() if available
|
|
if (function_exists('getallheaders')) {
|
|
$headers = getallheaders();
|
|
if (isset($headers['Authorization'])) {
|
|
return $headers['Authorization'];
|
|
}
|
|
if (isset($headers['authorization'])) {
|
|
return $headers['authorization'];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Extract Bearer token from Authorization header
|
|
*/
|
|
private function extract_bearer_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 token format validation
|
|
if (empty($token) || strlen($token) < 16) {
|
|
return null;
|
|
}
|
|
|
|
// Only allow alphanumeric characters and common token characters
|
|
if (!preg_match('/^[a-zA-Z0-9._-]+$/', $token)) {
|
|
return null;
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Validate token and return associated user ID
|
|
*/
|
|
private function validate_token(string $token): ?int {
|
|
// Use OAuth2Server for database-backed token validation
|
|
$oauth2_server = new OAuth2Server([]);
|
|
$token_data = $oauth2_server->validate_access_token($token);
|
|
|
|
if ($token_data) {
|
|
return (int) $token_data['user_id'];
|
|
}
|
|
|
|
// Fallback: Check against stored test token for backward compatibility
|
|
$test_token = get_option('wp_oauth2_poc_test_token');
|
|
if ($token === $test_token) {
|
|
// Return admin user for test token
|
|
$admin_users = get_users(['role' => 'administrator', 'number' => 1]);
|
|
if (!empty($admin_users)) {
|
|
return $admin_users[0]->ID;
|
|
}
|
|
}
|
|
|
|
// Legacy PoC token storage check
|
|
$stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []);
|
|
foreach ($stored_tokens as $stored_token) {
|
|
if ($stored_token['token'] === $token) {
|
|
// Check expiration
|
|
if (time() > $stored_token['expires']) {
|
|
continue; // Token expired
|
|
}
|
|
return (int) $stored_token['user_id'];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Check if user account is active
|
|
*/
|
|
private function is_user_active(\WP_User $user): bool {
|
|
// Check if user account is active
|
|
if (isset($user->user_status) && $user->user_status != 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check if user has required capabilities
|
|
if (!$user->exists()) {
|
|
return false;
|
|
}
|
|
|
|
// Additional checks can be added here
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate a new OAuth2 access token for a user (for PoC demonstration)
|
|
*/
|
|
public static function generate_token_for_user(int $user_id, int $expires_in = 3600): string {
|
|
$token = bin2hex(random_bytes(32));
|
|
$expires = time() + $expires_in;
|
|
|
|
$stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []);
|
|
$stored_tokens[] = [
|
|
'token' => $token,
|
|
'user_id' => $user_id,
|
|
'expires' => $expires,
|
|
'created' => time(),
|
|
'scope' => 'basic',
|
|
];
|
|
|
|
update_option(self::TOKEN_STORAGE_OPTION, $stored_tokens);
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Revoke a token
|
|
*/
|
|
public static function revoke_token(string $token): bool {
|
|
$stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []);
|
|
|
|
$stored_tokens = array_filter($stored_tokens, function($stored_token) use ($token) {
|
|
return $stored_token['token'] !== $token;
|
|
});
|
|
|
|
update_option(self::TOKEN_STORAGE_OPTION, array_values($stored_tokens));
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Clean up expired tokens
|
|
*/
|
|
public static function cleanup_expired_tokens(): int {
|
|
$stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []);
|
|
$current_time = time();
|
|
$removed_count = 0;
|
|
|
|
$active_tokens = array_filter($stored_tokens, function($token) use ($current_time, &$removed_count) {
|
|
if ($current_time > $token['expires']) {
|
|
$removed_count++;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
update_option(self::TOKEN_STORAGE_OPTION, array_values($active_tokens));
|
|
|
|
return $removed_count;
|
|
}
|
|
} |