- 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
566 lines
21 KiB
PHP
566 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* Main OAuth2 Proof of Concept Class
|
|
* Demonstrates secure WordPress authentication hook integration
|
|
* with extensible architecture for multiple authentication types
|
|
*
|
|
* @package WordPress OAuth2 PoC
|
|
*/
|
|
|
|
namespace WPOAuth2Server\Core;
|
|
|
|
use WPOAuth2Server\Auth\AuthenticatorInterface;
|
|
use WPOAuth2Server\Auth\OAuth2BearerAuthenticator;
|
|
use WPOAuth2Server\Auth\ApiKeyAuthenticator;
|
|
use WPOAuth2Server\Auth\JwtAuthenticator;
|
|
|
|
defined('ABSPATH') or die('Direct access forbidden.');
|
|
|
|
class OAuth2PoC {
|
|
|
|
/**
|
|
* Singleton instance
|
|
* @var OAuth2PoC|null
|
|
*/
|
|
private static ?OAuth2PoC $instance = null;
|
|
|
|
/**
|
|
* Registered authenticators
|
|
* @var AuthenticatorInterface[]
|
|
*/
|
|
private array $authenticators = [];
|
|
|
|
/**
|
|
* Plugin settings
|
|
* @var array
|
|
*/
|
|
private array $settings;
|
|
|
|
/**
|
|
* OAuth2 Server instance
|
|
* @var OAuth2Server
|
|
*/
|
|
private OAuth2Server $oauth2_server;
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
public static function instance(): self {
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Private constructor - Singleton pattern
|
|
*/
|
|
private function __construct() {
|
|
$this->load_settings();
|
|
$this->init_oauth2_server();
|
|
$this->register_default_authenticators();
|
|
$this->init_wordpress_hooks();
|
|
}
|
|
|
|
/**
|
|
* Load plugin settings with secure defaults
|
|
*/
|
|
private function load_settings(): void {
|
|
$defaults = [
|
|
'enabled' => true,
|
|
'oauth2_enabled' => true,
|
|
'api_key_enabled' => false,
|
|
'jwt_enabled' => false,
|
|
'block_unauthenticated_rest' => false,
|
|
'allowed_authentication_types' => ['oauth2'],
|
|
'require_https' => true,
|
|
'token_expiry' => 3600, // 1 hour
|
|
];
|
|
|
|
$stored_settings = get_option('wp_oauth2_poc_settings', []);
|
|
$this->settings = array_merge($defaults, $stored_settings);
|
|
}
|
|
|
|
/**
|
|
* Initialize OAuth2 Server
|
|
*/
|
|
private function init_oauth2_server(): void {
|
|
$this->oauth2_server = new OAuth2Server($this->settings);
|
|
}
|
|
|
|
/**
|
|
* Register default authenticators
|
|
*/
|
|
private function register_default_authenticators(): void {
|
|
// OAuth2 Bearer Token Authenticator
|
|
if ($this->settings['oauth2_enabled']) {
|
|
$this->register_authenticator(new OAuth2BearerAuthenticator());
|
|
}
|
|
|
|
// API Key Authenticator (for future extension)
|
|
if ($this->settings['api_key_enabled']) {
|
|
$this->register_authenticator(new ApiKeyAuthenticator());
|
|
}
|
|
|
|
// JWT Token Authenticator (for future extension)
|
|
if ($this->settings['jwt_enabled']) {
|
|
$this->register_authenticator(new JwtAuthenticator());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize WordPress hooks
|
|
*/
|
|
private function init_wordpress_hooks(): void {
|
|
// Core authentication hook - THE PATTERN FROM ORIGINAL PLUGIN
|
|
add_filter('determine_current_user', [$this, 'authenticate_user'], 20);
|
|
|
|
// REST API protection
|
|
add_filter('rest_authentication_errors', [$this, 'protect_rest_api']);
|
|
|
|
// URL rewrite rules for OAuth endpoints
|
|
add_action('init', [$this, 'register_rewrite_rules']);
|
|
|
|
// Handle OAuth endpoints
|
|
add_action('template_redirect', [$this, 'handle_oauth_requests']);
|
|
|
|
// Admin interface hooks
|
|
if (is_admin()) {
|
|
add_action('admin_menu', [$this, 'add_admin_menu']);
|
|
add_action('admin_init', [$this, 'register_settings']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register an authenticator
|
|
* This allows extending the system with new authentication types
|
|
*/
|
|
public function register_authenticator(AuthenticatorInterface $authenticator): void {
|
|
$this->authenticators[$authenticator->get_name()] = $authenticator;
|
|
}
|
|
|
|
/**
|
|
* Main authentication method - WordPress determine_current_user filter
|
|
*
|
|
* This is the CORE PATTERN from the original plugin, implemented securely
|
|
*/
|
|
public function authenticate_user(?int $user_id): ?int {
|
|
// Respect existing authentication FIRST (non-destructive pattern)
|
|
if ($user_id && $user_id > 0) {
|
|
return $user_id;
|
|
}
|
|
|
|
// Check if authentication is enabled
|
|
if (!$this->settings['enabled']) {
|
|
return $user_id;
|
|
}
|
|
|
|
// HTTPS enforcement for production
|
|
if ($this->settings['require_https'] && !is_ssl() && !wp_get_environment_type() === 'development') {
|
|
return $user_id;
|
|
}
|
|
|
|
// Try each registered authenticator
|
|
foreach ($this->authenticators as $authenticator) {
|
|
if (!$authenticator->can_authenticate()) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$authenticated_user_id = $authenticator->authenticate();
|
|
|
|
if ($authenticated_user_id && $authenticated_user_id > 0) {
|
|
// Log successful authentication for security monitoring
|
|
$this->log_authentication_event($authenticator->get_name(), $authenticated_user_id, 'success');
|
|
|
|
return (int) $authenticated_user_id;
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Log authentication failure
|
|
$this->log_authentication_event($authenticator->get_name(), null, 'failure', $e->getMessage());
|
|
|
|
// Continue to next authenticator
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// CRITICAL: Return false (not null) for WordPress compatibility
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Protect REST API endpoints based on settings
|
|
*/
|
|
public function protect_rest_api($result) {
|
|
if (!$this->settings['block_unauthenticated_rest']) {
|
|
return $result;
|
|
}
|
|
|
|
// Allow if user is already authenticated
|
|
if (is_user_logged_in()) {
|
|
return $result;
|
|
}
|
|
|
|
// Block unauthenticated requests
|
|
return new \WP_Error(
|
|
'rest_not_authorized',
|
|
__('Authentication required to access this endpoint.', 'wp-oauth2-poc'),
|
|
['status' => 401]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register OAuth2 endpoint rewrite rules
|
|
*/
|
|
public function register_rewrite_rules(): void {
|
|
// Standard OAuth2 endpoints
|
|
add_rewrite_rule('^oauth/token/?$', 'index.php?oauth_endpoint=token', 'top');
|
|
add_rewrite_rule('^oauth/authorize/?$', 'index.php?oauth_endpoint=authorize', 'top');
|
|
add_rewrite_rule('^oauth/revoke/?$', 'index.php?oauth_endpoint=revoke', 'top');
|
|
|
|
// OpenID Connect discovery
|
|
add_rewrite_rule('^\.well-known/oauth-authorization-server/?$', 'index.php?oauth_endpoint=metadata', 'top');
|
|
|
|
// Register query variables
|
|
global $wp;
|
|
$wp->add_query_var('oauth_endpoint');
|
|
}
|
|
|
|
/**
|
|
* Handle OAuth endpoint requests
|
|
*/
|
|
public function handle_oauth_requests(): void {
|
|
global $wp_query;
|
|
|
|
$endpoint = $wp_query->get('oauth_endpoint');
|
|
if (!$endpoint) {
|
|
return;
|
|
}
|
|
|
|
// Define constant for OAuth context
|
|
if (!defined('DOING_OAUTH')) {
|
|
define('DOING_OAUTH', true);
|
|
}
|
|
|
|
// Route to appropriate endpoint handler
|
|
switch ($endpoint) {
|
|
case 'token':
|
|
$this->handle_token_endpoint();
|
|
break;
|
|
case 'authorize':
|
|
$this->handle_authorize_endpoint();
|
|
break;
|
|
case 'revoke':
|
|
$this->handle_revoke_endpoint();
|
|
break;
|
|
case 'metadata':
|
|
$this->handle_metadata_endpoint();
|
|
break;
|
|
default:
|
|
$this->send_error_response(404, 'invalid_endpoint', 'Unknown OAuth endpoint');
|
|
}
|
|
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Handle token endpoint
|
|
*/
|
|
private function handle_token_endpoint(): void {
|
|
$this->oauth2_server->handle_token_request();
|
|
}
|
|
|
|
/**
|
|
* Generate cryptographically secure token
|
|
*/
|
|
private function generate_secure_token(): string {
|
|
// Use cryptographically secure random bytes (fixes original plugin's weakness)
|
|
return bin2hex(random_bytes(32));
|
|
}
|
|
|
|
/**
|
|
* Handle authorize endpoint
|
|
*/
|
|
private function handle_authorize_endpoint(): void {
|
|
// Check if this is a form submission
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$this->oauth2_server->process_authorization_form();
|
|
} else {
|
|
$this->oauth2_server->handle_authorization_request();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle revoke endpoint placeholder
|
|
*/
|
|
private function handle_revoke_endpoint(): void {
|
|
$this->send_error_response(501, 'not_implemented', 'Revoke endpoint not yet implemented in PoC');
|
|
}
|
|
|
|
/**
|
|
* Handle OAuth metadata endpoint
|
|
*/
|
|
private function handle_metadata_endpoint(): void {
|
|
$metadata = [
|
|
'issuer' => home_url(),
|
|
'token_endpoint' => home_url('oauth/token'),
|
|
'authorization_endpoint' => home_url('oauth/authorize'),
|
|
'revocation_endpoint' => home_url('oauth/revoke'),
|
|
'response_types_supported' => ['code'],
|
|
'grant_types_supported' => ['authorization_code'],
|
|
'token_endpoint_auth_methods_supported' => ['client_secret_basic'],
|
|
'code_challenge_methods_supported' => ['S256'], // Only secure PKCE method
|
|
];
|
|
|
|
$this->send_json_response($metadata);
|
|
}
|
|
|
|
/**
|
|
* Send JSON response with proper headers
|
|
*/
|
|
private function send_json_response(array $data, int $status_code = 200): void {
|
|
status_header($status_code);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
header('Expires: 0');
|
|
|
|
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
/**
|
|
* Send OAuth2 error response
|
|
*/
|
|
private function send_error_response(int $status_code, string $error, string $description): void {
|
|
$error_data = [
|
|
'error' => $error,
|
|
'error_description' => $description,
|
|
];
|
|
|
|
$this->send_json_response($error_data, $status_code);
|
|
}
|
|
|
|
/**
|
|
* Log authentication events for security monitoring
|
|
*/
|
|
private function log_authentication_event(string $auth_type, ?int $user_id, string $status, string $details = ''): void {
|
|
$log_entry = [
|
|
'timestamp' => current_time('mysql'),
|
|
'auth_type' => $auth_type,
|
|
'user_id' => $user_id,
|
|
'status' => $status,
|
|
'details' => $details,
|
|
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
|
];
|
|
|
|
// Store in WordPress options for PoC (in production, use proper logging)
|
|
$existing_logs = get_option('wp_oauth2_poc_auth_logs', []);
|
|
$existing_logs[] = $log_entry;
|
|
|
|
// Keep only last 100 entries for PoC
|
|
if (count($existing_logs) > 100) {
|
|
$existing_logs = array_slice($existing_logs, -100);
|
|
}
|
|
|
|
update_option('wp_oauth2_poc_auth_logs', $existing_logs);
|
|
}
|
|
|
|
/**
|
|
* Add admin menu
|
|
*/
|
|
public function add_admin_menu(): void {
|
|
add_options_page(
|
|
__('OAuth2 PoC Settings', 'wp-oauth2-poc'),
|
|
__('OAuth2 PoC', 'wp-oauth2-poc'),
|
|
'manage_options',
|
|
'wp-oauth2-poc',
|
|
[$this, 'render_admin_page']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register settings
|
|
*/
|
|
public function register_settings(): void {
|
|
register_setting('wp_oauth2_poc_settings', 'wp_oauth2_poc_settings');
|
|
}
|
|
|
|
/**
|
|
* Render admin page
|
|
*/
|
|
public function render_admin_page(): void {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.'));
|
|
}
|
|
|
|
// Handle client creation
|
|
if ($_POST && wp_verify_nonce($_POST['oauth2_nonce'] ?? '', 'create_client')) {
|
|
$this->handle_client_creation();
|
|
}
|
|
|
|
// Handle client deletion
|
|
if (isset($_GET['delete_client']) && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'delete_client')) {
|
|
$this->handle_client_deletion($_GET['delete_client']);
|
|
}
|
|
|
|
$client_manager = new OAuth2ClientManager();
|
|
$user_clients = $client_manager->get_user_clients();
|
|
$stats = $client_manager->get_statistics();
|
|
|
|
echo '<div class="wrap">';
|
|
echo '<h1>' . __('OAuth2 Server Management', 'wp-oauth2-poc') . '</h1>';
|
|
|
|
// Statistics overview
|
|
echo '<div class="card">';
|
|
echo '<h2>' . __('Server Statistics', 'wp-oauth2-poc') . '</h2>';
|
|
echo '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">';
|
|
echo '<div><strong>' . __('Total Clients:', 'wp-oauth2-poc') . '</strong> ' . esc_html($stats['total_clients']) . '</div>';
|
|
echo '<div><strong>' . __('Active Tokens:', 'wp-oauth2-poc') . '</strong> ' . esc_html($stats['active_tokens']) . '</div>';
|
|
echo '<div><strong>' . __('Today\'s Authorizations:', 'wp-oauth2-poc') . '</strong> ' . esc_html($stats['todays_authorizations']) . '</div>';
|
|
echo '<div><strong>' . __('Public Clients:', 'wp-oauth2-poc') . '</strong> ' . esc_html($stats['public_clients']) . '</div>';
|
|
echo '</div>';
|
|
echo '</div>';
|
|
|
|
// OAuth2 endpoints information
|
|
echo '<div class="card">';
|
|
echo '<h2>' . __('OAuth2 Endpoints', 'wp-oauth2-poc') . '</h2>';
|
|
echo '<table class="widefat">';
|
|
echo '<tr><th>Endpoint</th><th>URL</th><th>Method</th></tr>';
|
|
echo '<tr><td>Authorization</td><td><code>' . esc_html(home_url('oauth/authorize')) . '</code></td><td>GET</td></tr>';
|
|
echo '<tr><td>Token</td><td><code>' . esc_html(home_url('oauth/token')) . '</code></td><td>POST</td></tr>';
|
|
echo '<tr><td>Metadata</td><td><code>' . esc_html(home_url('.well-known/oauth-authorization-server')) . '</code></td><td>GET</td></tr>';
|
|
echo '</table>';
|
|
echo '</div>';
|
|
|
|
// Client management
|
|
echo '<div class="card">';
|
|
echo '<h2>' . __('Create New Client', 'wp-oauth2-poc') . '</h2>';
|
|
echo '<form method="post">';
|
|
wp_nonce_field('create_client', 'oauth2_nonce');
|
|
echo '<table class="form-table">';
|
|
echo '<tr><th><label for="client_name">Client Name</label></th>';
|
|
echo '<td><input type="text" id="client_name" name="client_name" required style="width: 300px;" /></td></tr>';
|
|
echo '<tr><th><label for="redirect_uri">Redirect URI</label></th>';
|
|
echo '<td><input type="url" id="redirect_uri" name="redirect_uri" required style="width: 400px;" placeholder="https://example.com/callback" /></td></tr>';
|
|
echo '<tr><th><label for="scope">Scope</label></th>';
|
|
echo '<td><input type="text" id="scope" name="scope" value="basic" style="width: 300px;" /></td></tr>';
|
|
echo '<tr><th><label for="is_public">Client Type</label></th>';
|
|
echo '<td><label><input type="checkbox" id="is_public" name="is_public" /> Public Client (mobile/SPA)</label></td></tr>';
|
|
echo '</table>';
|
|
echo '<p><button type="submit" class="button button-primary">Create Client</button></p>';
|
|
echo '</form>';
|
|
echo '</div>';
|
|
|
|
// Existing clients
|
|
if (!empty($user_clients)) {
|
|
echo '<div class="card">';
|
|
echo '<h2>' . __('Your OAuth2 Clients', 'wp-oauth2-poc') . '</h2>';
|
|
echo '<table class="widefat">';
|
|
echo '<thead><tr><th>Name</th><th>Client ID</th><th>Redirect URI</th><th>Type</th><th>Created</th><th>Actions</th></tr></thead>';
|
|
echo '<tbody>';
|
|
foreach ($user_clients as $client) {
|
|
echo '<tr>';
|
|
echo '<td><strong>' . esc_html($client['client_name']) . '</strong></td>';
|
|
echo '<td><code>' . esc_html($client['client_id']) . '</code></td>';
|
|
echo '<td><code>' . esc_html($client['redirect_uri']) . '</code></td>';
|
|
echo '<td>' . esc_html($client['is_public'] ? 'Public' : 'Confidential') . '</td>';
|
|
echo '<td>' . esc_html($client['created_at']) . '</td>';
|
|
echo '<td>';
|
|
$delete_url = wp_nonce_url(
|
|
add_query_arg(['delete_client' => $client['client_id']]),
|
|
'delete_client'
|
|
);
|
|
echo '<a href="' . esc_url($delete_url) . '" onclick="return confirm(\'Are you sure?\')" class="button button-small">Delete</a>';
|
|
echo '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
echo '</div>';
|
|
}
|
|
|
|
// Testing section
|
|
echo '<div class="card">';
|
|
echo '<h2>' . __('Testing & Development', 'wp-oauth2-poc') . '</h2>';
|
|
echo '<p><a href="' . esc_url(plugin_dir_url(WP_OAUTH2_POC_FILE) . 'test-oauth2-flow.php?run_test=1') . '" target="_blank" class="button">Run OAuth2 Flow Test</a></p>';
|
|
|
|
// Show test token for backward compatibility
|
|
$test_token = get_option('wp_oauth2_poc_test_token');
|
|
if ($test_token) {
|
|
echo '<p><strong>' . __('Legacy Test Token:', 'wp-oauth2-poc') . '</strong> <code>' . esc_html($test_token) . '</code></p>';
|
|
echo '<p><em>Use this token for testing: <code>Authorization: Bearer ' . esc_html($test_token) . '</code></em></p>';
|
|
}
|
|
echo '</div>';
|
|
|
|
// Authentication logs
|
|
$logs = get_option('wp_oauth2_poc_auth_logs', []);
|
|
if (!empty($logs)) {
|
|
echo '<div class="card">';
|
|
echo '<h2>' . __('Recent Authentication Events', 'wp-oauth2-poc') . '</h2>';
|
|
echo '<table class="widefat">';
|
|
echo '<thead><tr><th>Timestamp</th><th>Type</th><th>User ID</th><th>Status</th><th>IP</th></tr></thead>';
|
|
echo '<tbody>';
|
|
foreach (array_reverse(array_slice($logs, -10)) as $log) {
|
|
echo '<tr>';
|
|
echo '<td>' . esc_html($log['timestamp']) . '</td>';
|
|
echo '<td>' . esc_html($log['auth_type']) . '</td>';
|
|
echo '<td>' . esc_html($log['user_id'] ?? 'N/A') . '</td>';
|
|
echo '<td>' . esc_html($log['status']) . '</td>';
|
|
echo '<td>' . esc_html($log['ip_address']) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
echo '</div>';
|
|
}
|
|
|
|
echo '</div>';
|
|
}
|
|
|
|
/**
|
|
* Handle client creation
|
|
*/
|
|
private function handle_client_creation(): void {
|
|
try {
|
|
$client_manager = new OAuth2ClientManager();
|
|
$client = $client_manager->create_client([
|
|
'client_name' => sanitize_text_field($_POST['client_name']),
|
|
'redirect_uri' => esc_url_raw($_POST['redirect_uri']),
|
|
'scope' => sanitize_text_field($_POST['scope'] ?? 'basic'),
|
|
'is_public' => !empty($_POST['is_public']),
|
|
]);
|
|
|
|
echo '<div class="notice notice-success"><p>';
|
|
echo '<strong>Client created successfully!</strong><br>';
|
|
echo 'Client ID: <code>' . esc_html($client['client_id']) . '</code><br>';
|
|
echo 'Client Secret: <code>' . esc_html($client['client_secret']) . '</code><br>';
|
|
echo '<em>Save the client secret - it won\'t be shown again!</em>';
|
|
echo '</p></div>';
|
|
|
|
} catch (Exception $e) {
|
|
echo '<div class="notice notice-error"><p>Error creating client: ' . esc_html($e->getMessage()) . '</p></div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle client deletion
|
|
*/
|
|
private function handle_client_deletion(string $client_id): void {
|
|
try {
|
|
$client_manager = new OAuth2ClientManager();
|
|
if ($client_manager->delete_client($client_id)) {
|
|
echo '<div class="notice notice-success"><p>Client deleted successfully!</p></div>';
|
|
} else {
|
|
echo '<div class="notice notice-error"><p>Failed to delete client.</p></div>';
|
|
}
|
|
} catch (Exception $e) {
|
|
echo '<div class="notice notice-error"><p>Error deleting client: ' . esc_html($e->getMessage()) . '</p></div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get plugin settings
|
|
*/
|
|
public function get_setting(string $key, $default = null) {
|
|
return $this->settings[$key] ?? $default;
|
|
}
|
|
} |