- 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
193 lines
5.6 KiB
PHP
193 lines
5.6 KiB
PHP
<?php
|
|
/**
|
|
* API Key Authenticator
|
|
* Implements API key authentication for WordPress
|
|
*
|
|
* @package WordPress Authentication Framework PoC
|
|
*/
|
|
|
|
namespace WPOAuth2Server\Auth;
|
|
|
|
defined('ABSPATH') or die('Direct access forbidden.');
|
|
|
|
class ApiKeyAuthenticator implements AuthenticatorInterface {
|
|
|
|
/**
|
|
* API key storage option name
|
|
*/
|
|
private const API_KEY_STORAGE_OPTION = 'wp_auth_framework_api_keys';
|
|
|
|
public function get_name(): string {
|
|
return 'api_key';
|
|
}
|
|
|
|
public function get_display_name(): string {
|
|
return __('API Key Authentication', 'wp-oauth2-poc');
|
|
}
|
|
|
|
public function get_description(): string {
|
|
return __('Authenticates users using API keys passed in X-API-Key header or api_key parameter', 'wp-oauth2-poc');
|
|
}
|
|
|
|
public function can_authenticate(): bool {
|
|
return $this->get_api_key() !== null;
|
|
}
|
|
|
|
public function authenticate(): ?int {
|
|
$api_key = $this->get_api_key();
|
|
if (!$api_key) {
|
|
return null;
|
|
}
|
|
|
|
$user_id = $this->validate_api_key($api_key);
|
|
if (!$user_id) {
|
|
throw new \Exception('Invalid API key');
|
|
}
|
|
|
|
// Verify user still exists and is active
|
|
$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 {
|
|
$api_key = $this->get_api_key();
|
|
return $api_key && $this->validate_api_key($api_key) !== null;
|
|
}
|
|
|
|
public function get_priority(): int {
|
|
return 20; // Lower priority than OAuth2
|
|
}
|
|
|
|
public function requires_https(): bool {
|
|
return true; // API keys should require HTTPS
|
|
}
|
|
|
|
public function get_allowed_methods(): array {
|
|
return ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'];
|
|
}
|
|
|
|
public function get_response_headers(): array {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Get API key from request (header or parameter)
|
|
*/
|
|
private function get_api_key(): ?string {
|
|
// Check X-API-Key header
|
|
if (isset($_SERVER['HTTP_X_API_KEY'])) {
|
|
return sanitize_text_field($_SERVER['HTTP_X_API_KEY']);
|
|
}
|
|
|
|
// Check api_key parameter
|
|
if (isset($_GET['api_key'])) {
|
|
return sanitize_text_field($_GET['api_key']);
|
|
}
|
|
|
|
if (isset($_POST['api_key'])) {
|
|
return sanitize_text_field($_POST['api_key']);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate API key and return user ID
|
|
*/
|
|
private function validate_api_key(string $api_key): ?int {
|
|
$stored_keys = get_option(self::API_KEY_STORAGE_OPTION, []);
|
|
|
|
foreach ($stored_keys as $key_data) {
|
|
if (hash_equals($key_data['key'], $api_key)) {
|
|
// Check if key is active
|
|
if (!$key_data['active']) {
|
|
continue;
|
|
}
|
|
|
|
// Check expiration if set
|
|
if (isset($key_data['expires']) && time() > $key_data['expires']) {
|
|
continue;
|
|
}
|
|
|
|
// Update last used timestamp
|
|
$this->update_last_used($api_key);
|
|
|
|
return (int) $key_data['user_id'];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Update last used timestamp for API key
|
|
*/
|
|
private function update_last_used(string $api_key): void {
|
|
$stored_keys = get_option(self::API_KEY_STORAGE_OPTION, []);
|
|
|
|
foreach ($stored_keys as &$key_data) {
|
|
if (hash_equals($key_data['key'], $api_key)) {
|
|
$key_data['last_used'] = time();
|
|
break;
|
|
}
|
|
}
|
|
|
|
update_option(self::API_KEY_STORAGE_OPTION, $stored_keys);
|
|
}
|
|
|
|
/**
|
|
* Generate a new API key for a user
|
|
*/
|
|
public static function generate_api_key_for_user(int $user_id, string $name = '', ?int $expires_in = null): string {
|
|
$api_key = 'wpak_' . bin2hex(random_bytes(24)); // 48 character key with prefix
|
|
$expires = $expires_in ? time() + $expires_in : null;
|
|
|
|
$stored_keys = get_option(self::API_KEY_STORAGE_OPTION, []);
|
|
$stored_keys[] = [
|
|
'key' => $api_key,
|
|
'user_id' => $user_id,
|
|
'name' => $name ?: 'API Key ' . date('Y-m-d H:i:s'),
|
|
'created' => time(),
|
|
'expires' => $expires,
|
|
'last_used' => null,
|
|
'active' => true,
|
|
];
|
|
|
|
update_option(self::API_KEY_STORAGE_OPTION, $stored_keys);
|
|
|
|
return $api_key;
|
|
}
|
|
|
|
/**
|
|
* Revoke an API key
|
|
*/
|
|
public static function revoke_api_key(string $api_key): bool {
|
|
$stored_keys = get_option(self::API_KEY_STORAGE_OPTION, []);
|
|
|
|
foreach ($stored_keys as &$key_data) {
|
|
if (hash_equals($key_data['key'], $api_key)) {
|
|
$key_data['active'] = false;
|
|
$key_data['revoked'] = time();
|
|
update_option(self::API_KEY_STORAGE_OPTION, $stored_keys);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* List API keys for a user
|
|
*/
|
|
public static function get_user_api_keys(int $user_id): array {
|
|
$stored_keys = get_option(self::API_KEY_STORAGE_OPTION, []);
|
|
|
|
return array_filter($stored_keys, function($key_data) use ($user_id) {
|
|
return $key_data['user_id'] === $user_id && $key_data['active'];
|
|
});
|
|
}
|
|
} |