tigerstyle-scent/Core/OAuth2PoC.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

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