tigerstyle-scent/Auth/OAuth2BearerAuthenticator.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

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