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
This commit is contained in:
Ryan Malloy 2025-09-16 20:53:00 -06:00
commit 1da0acd25a
10 changed files with 2691 additions and 0 deletions

184
Admin/class-wo-table.php Normal file
View File

@ -0,0 +1,184 @@
<?php
/**
* Custom WP Table for clients
*
* @author Justin Greer <justin@justin-greer.com>
* @package WP OAuth Server
*/
defined( 'ABSPATH' ) or die( 'No script kiddies please!' );
class WO_Table extends WP_List_Table {
/**
* Constructor, we override the parent to pass our own arguments
* We usually focus on three parameters: singular and plural labels, as well as whether the class supports AJAX.
*/
public function __construct() {
parent::__construct(
array(
'singular' => 'wp_list_text_link', // Singular label
'plural' => 'wp_list_test_links', // plural label, also this well be one of the table css class
'ajax' => false, // We won't support Ajax for this table
)
);
}
/**
* Add extra markup in the toolbars before or after the list.
*
* @param string $which , helps you decide if you add the markup after (bottom) or before (top) the list
*/
public function extra_tablenav( $which ) {
if ( $which == 'top' ) {
return false;
}
if ( $which == 'bottom' ) {
return false;
}
}
/**
* Overide default functionality to remove _nonce field.
*
* @return [type] [description]
*/
public function display_tablenav( $which ) {
?>
<div class="tablenav
<?php
echo esc_attr( $which );
?>
">
<div class="alignleft actions bulkactions">
<?php
$this->bulk_actions( $which );
?>
</div>
<?php
$this->extra_tablenav( $which );
$this->pagination( $which );
?>
<br class="clear"/>
</div>
<?php
}
/**
* Define the columns that are going to be used in the table.
*
* @return array $columns, the array of columns to use with the table
*/
public function get_columns() {
return $columns = array(
'title' => __( 'Name' ),
'client_id' => __( 'Client ID' ),
// 'client_secret' => __( 'Redirect URI' )
);
}
/**
* Decide which columns to activate the sorting functionality on.
*
* @return array $sortable, the array of columns that can be sorted by the user
*/
public function get_sortable_columns() {
return $sortable = array(
// 'name' => array('name'),
// 'user_id'=>array('user_id')
);
}
/**
* Prepare the table with different parameters, pagination, columns and table elements.
*
* @Updated 4.0.2. does not include user generated clients anymore via original query
*/
public function prepare_items() {
global $wpdb, $_wp_column_headers;
$screen = get_current_screen();
$query = "SELECT * FROM {$wpdb->prefix}posts WHERE post_type = 'wo_client' AND post_name NOT LIKE 'user_generated_%'";
$totalitems = $wpdb->query( $query );
$perpage = 5;
$paged = ! empty( $_GET['paged'] ) ? intval( $_GET['paged'] ) : '';
if ( empty( $paged ) || ! is_numeric( $paged ) || $paged <= 0 ) {
$paged = 1;
}
$totalpages = ceil( $totalitems / $perpage );
$this->set_pagination_args(
array(
'total_items' => $totalitems,
'total_pages' => $totalpages,
'per_page' => $perpage,
)
);
$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, $hidden, $sortable );
$results = $wpdb->get_results( $query );
$this->items = $results;
}
/**
* Display the rows of records in the table.
*
* @return string, echo the markup of the rows
*/
public function display_rows() {
// Get the records registered in the prepare_items method
$records = $this->items;
// Get the columns registered in the get_columns and get_sortable_columns methods
list( $columns, $hidden ) = $this->get_column_info();
// Loop for each record
if ( ! empty( $records ) ) {
foreach ( $records as $rec ) {
// Open the line
echo '<tr id="record_' . $rec->ID . '">';
foreach ( $columns as $column_name => $column_display_name ) {
// Style attributes for each col
$class = "class='$column_name column-$column_name'";
$style = '';
if ( in_array( $column_name, $hidden ) ) {
$style = ' style="display:none;"';
}
$attributes = $class . $style;
switch ( $column_name ) {
case 'title':
$edit_link = admin_url( 'admin.php?page=wo_edit_client&id=' . $rec->ID );
echo '<td ' . esc_attr( $attributes ) . '><strong><a href="' . esc_url( $edit_link ) . '" title="Edit">' . esc_html( $rec->post_title ) . '</a></strong>
<div class="row-actions">
<span class="edit"><a href="' . esc_url( $edit_link ) . '" title="' . __( 'Edit Client', 'wp-oauth' ) . '">' . __( 'Edit', 'wp-oauth' ) . '</a> | </span>
<span class="trash">
<a class="submitdelete" title="' . __( 'delete this client', 'wp-oauth' ) . '" onclick="wo_remove_client(' . $rec->ID . ',\''.wp_create_nonce( "remove_$rec->ID").'\')" href="#">' . __( 'Delete', 'wp-oauth' ) . '</a> </span>';
break;
// case "user_id": echo '<td '.$attributes.'>'.stripslashes($rec->user_id).'</td>'; break;
case 'client_id':
echo '<td ' . $attributes . '>' . get_post_meta( $rec->ID, 'client_id', true ) . '</td>';
break;
}
}
// Close the line
echo '</tr>';
}
}
}
}

View File

@ -0,0 +1,193 @@
<?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'];
});
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* Authenticator Interface
* Defines the contract for all authentication methods in WP OAuth Server CE
*
* @package WPOAuth2Server
* @subpackage Auth
* @version 1.0.0
*/
namespace WPOAuth2Server\Auth;
defined('ABSPATH') or die('Direct access forbidden.');
interface AuthenticatorInterface {
/**
* Get the unique name/identifier for this authenticator
* Used for logging, configuration, and registration
*/
public function get_name(): string;
/**
* Get human-readable display name for admin interface
*/
public function get_display_name(): string;
/**
* Get description of this authentication method
*/
public function get_description(): string;
/**
* Check if this authenticator can handle the current request
* This allows multiple authenticators to coexist and only activate when appropriate
*/
public function can_authenticate(): bool;
/**
* Perform authentication and return WordPress user ID
*
* @return int|null WordPress user ID if authentication successful, null if failed
* @throws \Exception If authentication fails with specific error
*/
public function authenticate(): ?int;
/**
* Validate the authentication credentials without actually authenticating
* Useful for token validation, API key verification, etc.
*/
public function validate_credentials(): bool;
/**
* Get authentication priority (lower = higher priority)
* Allows controlling the order in which authenticators are tried
*/
public function get_priority(): int;
/**
* Check if this authenticator requires HTTPS
*/
public function requires_https(): bool;
/**
* Get allowed HTTP methods for this authenticator
* @return array Array of allowed methods (e.g., ['GET', 'POST'])
*/
public function get_allowed_methods(): array;
/**
* Get any additional headers this authenticator needs to set
* @return array Associative array of header_name => header_value
*/
public function get_response_headers(): array;
}

253
Auth/JwtAuthenticator.php Normal file
View File

@ -0,0 +1,253 @@
<?php
/**
* JWT Token Authenticator
* Implements JWT (JSON Web Token) authentication for WordPress
*
* @package WordPress Authentication Framework PoC
*/
namespace WPOAuth2Server\Auth;
defined('ABSPATH') or die('Direct access forbidden.');
class JwtAuthenticator implements AuthenticatorInterface {
/**
* JWT secret option name
*/
private const JWT_SECRET_OPTION = 'wp_auth_framework_jwt_secret';
public function get_name(): string {
return 'jwt';
}
public function get_display_name(): string {
return __('JWT Token Authentication', 'wp-oauth2-poc');
}
public function get_description(): string {
return __('Authenticates users using JSON Web Tokens (JWT) in Authorization header', 'wp-oauth2-poc');
}
public function can_authenticate(): bool {
$auth_header = $this->get_authorization_header();
return $auth_header && strpos($auth_header, 'Bearer ') === 0 && $this->looks_like_jwt($auth_header);
}
public function authenticate(): ?int {
if (!$this->can_authenticate()) {
return null;
}
$jwt_token = $this->extract_jwt_token();
if (!$jwt_token) {
throw new \Exception('Invalid JWT token format');
}
$payload = $this->validate_jwt($jwt_token);
if (!$payload) {
throw new \Exception('Invalid or expired JWT token');
}
$user_id = $payload['sub'] ?? null;
if (!$user_id) {
throw new \Exception('JWT token missing user identifier');
}
// Verify user still exists
$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 {
if (!$this->can_authenticate()) {
return false;
}
$jwt_token = $this->extract_jwt_token();
return $jwt_token && $this->validate_jwt($jwt_token) !== null;
}
public function get_priority(): int {
return 15; // Between OAuth2 and API key
}
public function requires_https(): bool {
return true; // JWTs should require HTTPS
}
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 JWT"',
];
}
/**
* Get Authorization header from request
*/
private function get_authorization_header(): ?string {
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
return $_SERVER['HTTP_AUTHORIZATION'];
}
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
return $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
}
if (function_exists('getallheaders')) {
$headers = getallheaders();
if (isset($headers['Authorization'])) {
return $headers['Authorization'];
}
}
return null;
}
/**
* Check if the token looks like a JWT (has 3 parts separated by dots)
*/
private function looks_like_jwt(string $auth_header): bool {
$token = substr($auth_header, 7); // Remove "Bearer " prefix
return substr_count($token, '.') === 2;
}
/**
* Extract JWT token from Authorization header
*/
private function extract_jwt_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 JWT format validation (3 parts separated by dots)
if (substr_count($token, '.') !== 2) {
return null;
}
return $token;
}
/**
* Validate JWT token and return payload
* This is a simplified JWT validation for PoC - in production use a proper JWT library
*/
private function validate_jwt(string $jwt_token): ?array {
$parts = explode('.', $jwt_token);
if (count($parts) !== 3) {
return null;
}
[$header_encoded, $payload_encoded, $signature_encoded] = $parts;
// Decode header and payload
$header = json_decode($this->base64url_decode($header_encoded), true);
$payload = json_decode($this->base64url_decode($payload_encoded), true);
if (!$header || !$payload) {
return null;
}
// Check algorithm
if (($header['alg'] ?? '') !== 'HS256') {
return null;
}
// Verify signature
$secret = $this->get_jwt_secret();
$expected_signature = $this->base64url_encode(
hash_hmac('sha256', $header_encoded . '.' . $payload_encoded, $secret, true)
);
if (!hash_equals($expected_signature, $signature_encoded)) {
return null;
}
// Check expiration
if (isset($payload['exp']) && time() > $payload['exp']) {
return null;
}
// Check not before
if (isset($payload['nbf']) && time() < $payload['nbf']) {
return null;
}
// Check issued at
if (isset($payload['iat']) && time() < $payload['iat']) {
return null;
}
return $payload;
}
/**
* Get or generate JWT secret
*/
private function get_jwt_secret(): string {
$secret = get_option(self::JWT_SECRET_OPTION);
if (!$secret) {
$secret = bin2hex(random_bytes(32));
update_option(self::JWT_SECRET_OPTION, $secret);
}
return $secret;
}
/**
* Base64URL decode
*/
private function base64url_decode(string $data): string {
return base64_decode(strtr($data, '-_', '+/'));
}
/**
* Base64URL encode
*/
private function base64url_encode(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Generate a JWT token for a user
*/
public static function generate_jwt_for_user(int $user_id, int $expires_in = 3600): string {
$header = [
'typ' => 'JWT',
'alg' => 'HS256'
];
$payload = [
'iss' => home_url(),
'sub' => (string) $user_id,
'aud' => home_url(),
'iat' => time(),
'nbf' => time(),
'exp' => time() + $expires_in,
];
$header_encoded = (new self())->base64url_encode(json_encode($header));
$payload_encoded = (new self())->base64url_encode(json_encode($payload));
$secret = (new self())->get_jwt_secret();
$signature = (new self())->base64url_encode(
hash_hmac('sha256', $header_encoded . '.' . $payload_encoded, $secret, true)
);
return $header_encoded . '.' . $payload_encoded . '.' . $signature;
}
}

View File

@ -0,0 +1,259 @@
<?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;
}
}

View File

@ -0,0 +1,304 @@
<?php
/**
* OAuth2 Client Management
*
* Handles OAuth2 client registration and management
*
* @package WordPress OAuth2 PoC
*/
namespace WPOAuth2Server\Client;
defined('ABSPATH') or die('Direct access forbidden.');
class OAuth2ClientManager {
/**
* WordPress database instance
* @var \wpdb
*/
private \wpdb $wpdb;
/**
* Constructor
*/
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
}
/**
* Create a new OAuth2 client
*/
public function create_client(array $client_data): array {
// Validate required fields
$required_fields = ['client_name', 'redirect_uri'];
foreach ($required_fields as $field) {
if (empty($client_data[$field])) {
throw new \InvalidArgumentException("Missing required field: {$field}");
}
}
// Generate client ID and secret
$client_id = $this->generate_client_id();
$client_secret = $this->generate_client_secret();
// Sanitize and prepare data
$insert_data = [
'client_id' => $client_id,
'client_secret' => hash('sha256', $client_secret), // Hash the secret for storage
'client_name' => sanitize_text_field($client_data['client_name']),
'redirect_uri' => esc_url_raw($client_data['redirect_uri']),
'grant_types' => sanitize_text_field($client_data['grant_types'] ?? 'authorization_code'),
'scope' => sanitize_text_field($client_data['scope'] ?? 'basic'),
'user_id' => get_current_user_id(),
'is_public' => !empty($client_data['is_public']) ? 1 : 0,
];
// Insert into database
$result = $this->wpdb->insert(
$this->wpdb->prefix . 'oauth2_clients',
$insert_data,
['%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d']
);
if ($result === false) {
throw new \Exception('Failed to create OAuth2 client');
}
// Return client information (including plain text secret)
return [
'client_id' => $client_id,
'client_secret' => $client_secret, // Return plain text secret (only time it's available)
'client_name' => $insert_data['client_name'],
'redirect_uri' => $insert_data['redirect_uri'],
'grant_types' => $insert_data['grant_types'],
'scope' => $insert_data['scope'],
'is_public' => (bool) $insert_data['is_public'],
];
}
/**
* Get all clients for current user
*/
public function get_user_clients(int $user_id = null): array {
if ($user_id === null) {
$user_id = get_current_user_id();
}
$results = $this->wpdb->get_results(
$this->wpdb->prepare(
"SELECT client_id, client_name, redirect_uri, grant_types, scope, is_public, created_at
FROM {$this->wpdb->prefix}oauth2_clients
WHERE user_id = %d
ORDER BY created_at DESC",
$user_id
),
ARRAY_A
);
return $results ?: [];
}
/**
* Get client by ID
*/
public function get_client(string $client_id): ?array {
$result = $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->wpdb->prefix}oauth2_clients WHERE client_id = %s",
$client_id
),
ARRAY_A
);
return $result ?: null;
}
/**
* Delete client
*/
public function delete_client(string $client_id, int $user_id = null): bool {
if ($user_id === null) {
$user_id = get_current_user_id();
}
// Only allow deletion by client owner or admin
if (!current_user_can('manage_options')) {
$client = $this->get_client($client_id);
if (!$client || $client['user_id'] != $user_id) {
return false;
}
}
// Delete associated tokens and codes first
$this->cleanup_client_data($client_id);
// Delete client
$result = $this->wpdb->delete(
$this->wpdb->prefix . 'oauth2_clients',
['client_id' => $client_id],
['%s']
);
return $result !== false;
}
/**
* Clean up all data associated with a client
*/
private function cleanup_client_data(string $client_id): void {
// Delete access tokens
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth2_access_tokens',
['client_id' => $client_id],
['%s']
);
// Delete refresh tokens
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth2_refresh_tokens',
['client_id' => $client_id],
['%s']
);
// Delete authorization codes
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth2_authorization_codes',
['client_id' => $client_id],
['%s']
);
}
/**
* Generate client ID
*/
private function generate_client_id(): string {
return 'client_' . bin2hex(random_bytes(16));
}
/**
* Generate client secret
*/
private function generate_client_secret(): string {
return bin2hex(random_bytes(32));
}
/**
* Update client information
*/
public function update_client(string $client_id, array $update_data, int $user_id = null): bool {
if ($user_id === null) {
$user_id = get_current_user_id();
}
// Only allow update by client owner or admin
if (!current_user_can('manage_options')) {
$client = $this->get_client($client_id);
if (!$client || $client['user_id'] != $user_id) {
return false;
}
}
// Prepare update data
$allowed_fields = ['client_name', 'redirect_uri', 'grant_types', 'scope', 'is_public'];
$update_values = [];
$update_format = [];
foreach ($allowed_fields as $field) {
if (isset($update_data[$field])) {
switch ($field) {
case 'client_name':
case 'grant_types':
case 'scope':
$update_values[$field] = sanitize_text_field($update_data[$field]);
$update_format[] = '%s';
break;
case 'redirect_uri':
$update_values[$field] = esc_url_raw($update_data[$field]);
$update_format[] = '%s';
break;
case 'is_public':
$update_values[$field] = !empty($update_data[$field]) ? 1 : 0;
$update_format[] = '%d';
break;
}
}
}
if (empty($update_values)) {
return false;
}
$update_values['updated_at'] = current_time('mysql');
$update_format[] = '%s';
$result = $this->wpdb->update(
$this->wpdb->prefix . 'oauth2_clients',
$update_values,
['client_id' => $client_id],
$update_format,
['%s']
);
return $result !== false;
}
/**
* Get statistics for admin dashboard
*/
public function get_statistics(): array {
$stats = [];
// Total clients
$stats['total_clients'] = $this->wpdb->get_var(
"SELECT COUNT(*) FROM {$this->wpdb->prefix}oauth2_clients"
);
// Active tokens (non-expired)
$stats['active_tokens'] = $this->wpdb->get_var(
"SELECT COUNT(*) FROM {$this->wpdb->prefix}oauth2_access_tokens WHERE expires > NOW()"
);
// Today's authorizations
$stats['todays_authorizations'] = $this->wpdb->get_var(
"SELECT COUNT(*) FROM {$this->wpdb->prefix}oauth2_authorization_codes
WHERE DATE(created_at) = CURDATE()"
);
// Client types
$stats['public_clients'] = $this->wpdb->get_var(
"SELECT COUNT(*) FROM {$this->wpdb->prefix}oauth2_clients WHERE is_public = 1"
);
$stats['confidential_clients'] = $this->wpdb->get_var(
"SELECT COUNT(*) FROM {$this->wpdb->prefix}oauth2_clients WHERE is_public = 0"
);
return array_map('intval', $stats);
}
/**
* Clean up expired tokens and codes
*/
public function cleanup_expired_data(): array {
$cleanup_stats = [];
// Clean expired access tokens
$cleanup_stats['access_tokens'] = $this->wpdb->query(
"DELETE FROM {$this->wpdb->prefix}oauth2_access_tokens WHERE expires < NOW()"
);
// Clean expired refresh tokens
$cleanup_stats['refresh_tokens'] = $this->wpdb->query(
"DELETE FROM {$this->wpdb->prefix}oauth2_refresh_tokens WHERE expires < NOW()"
);
// Clean expired authorization codes
$cleanup_stats['authorization_codes'] = $this->wpdb->query(
"DELETE FROM {$this->wpdb->prefix}oauth2_authorization_codes WHERE expires < NOW()"
);
return $cleanup_stats;
}
}

566
Core/OAuth2PoC.php Normal file
View File

@ -0,0 +1,566 @@
<?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;
}
}

657
Core/OAuth2Server.php Normal file
View File

@ -0,0 +1,657 @@
<?php
/**
* OAuth2 Server Implementation
*
* Implements secure OAuth2 authorization server functionality
* with support for authorization code flow with PKCE
*
* @package WordPress OAuth2 PoC
*/
namespace WPOAuth2Server\Core;
defined('ABSPATH') or die('Direct access forbidden.');
class OAuth2Server {
/**
* WordPress database instance
* @var \wpdb
*/
private \wpdb $wpdb;
/**
* Plugin settings
* @var array
*/
private array $settings;
/**
* Constructor
*/
public function __construct(array $settings) {
global $wpdb;
$this->wpdb = $wpdb;
$this->settings = $settings;
}
/**
* Create OAuth2 database tables
*/
public function create_tables(): void {
$charset_collate = $this->wpdb->get_charset_collate();
// OAuth2 Clients table
$clients_table = $this->wpdb->prefix . 'oauth2_clients';
$sql_clients = "CREATE TABLE $clients_table (
client_id varchar(80) NOT NULL,
client_secret varchar(255) NOT NULL,
client_name varchar(255) NOT NULL,
redirect_uri text NOT NULL,
grant_types varchar(255) DEFAULT 'authorization_code',
scope text,
user_id bigint(20) NOT NULL,
is_public tinyint(1) DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (client_id),
KEY user_id (user_id)
) $charset_collate;";
// Authorization Codes table
$auth_codes_table = $this->wpdb->prefix . 'oauth2_authorization_codes';
$sql_auth_codes = "CREATE TABLE $auth_codes_table (
authorization_code varchar(255) NOT NULL,
client_id varchar(80) NOT NULL,
user_id bigint(20) NOT NULL,
redirect_uri text,
expires datetime NOT NULL,
scope text,
code_challenge varchar(255),
code_challenge_method varchar(20),
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (authorization_code),
KEY client_id (client_id),
KEY user_id (user_id),
KEY expires (expires)
) $charset_collate;";
// Access Tokens table
$access_tokens_table = $this->wpdb->prefix . 'oauth2_access_tokens';
$sql_access_tokens = "CREATE TABLE $access_tokens_table (
access_token varchar(255) NOT NULL,
client_id varchar(80) NOT NULL,
user_id bigint(20) NOT NULL,
expires datetime NOT NULL,
scope text,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (access_token),
KEY client_id (client_id),
KEY user_id (user_id),
KEY expires (expires)
) $charset_collate;";
// Refresh Tokens table
$refresh_tokens_table = $this->wpdb->prefix . 'oauth2_refresh_tokens';
$sql_refresh_tokens = "CREATE TABLE $refresh_tokens_table (
refresh_token varchar(255) NOT NULL,
client_id varchar(80) NOT NULL,
user_id bigint(20) NOT NULL,
expires datetime NOT NULL,
scope text,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (refresh_token),
KEY client_id (client_id),
KEY user_id (user_id),
KEY expires (expires)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql_clients);
dbDelta($sql_auth_codes);
dbDelta($sql_access_tokens);
dbDelta($sql_refresh_tokens);
}
/**
* Handle authorization endpoint request
*/
public function handle_authorization_request(): void {
// Validate request method
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
$this->send_error_response(405, 'invalid_request', 'Authorization endpoint requires GET method');
return;
}
// Extract and validate parameters
$client_id = sanitize_text_field($_GET['client_id'] ?? '');
$redirect_uri = esc_url_raw($_GET['redirect_uri'] ?? '');
$response_type = sanitize_text_field($_GET['response_type'] ?? '');
$scope = sanitize_text_field($_GET['scope'] ?? '');
$state = sanitize_text_field($_GET['state'] ?? '');
$code_challenge = sanitize_text_field($_GET['code_challenge'] ?? '');
$code_challenge_method = sanitize_text_field($_GET['code_challenge_method'] ?? '');
// Validate required parameters
if (empty($client_id) || empty($redirect_uri) || empty($response_type)) {
$this->send_error_response(400, 'invalid_request', 'Missing required parameters');
return;
}
// Validate response type
if ($response_type !== 'code') {
$this->send_error_response(400, 'unsupported_response_type', 'Only authorization code flow is supported');
return;
}
// Validate client
$client = $this->get_client($client_id);
if (!$client) {
$this->send_error_response(400, 'invalid_client', 'Invalid client_id');
return;
}
// Validate redirect URI
if (!$this->validate_redirect_uri($client, $redirect_uri)) {
$this->send_error_response(400, 'invalid_redirect_uri', 'Invalid redirect_uri');
return;
}
// Validate PKCE (required for public clients)
if ($client['is_public'] && (empty($code_challenge) || $code_challenge_method !== 'S256')) {
$this->send_authorization_error($redirect_uri, 'invalid_request', 'PKCE required for public clients', $state);
return;
}
// Check if user is logged in
if (!is_user_logged_in()) {
// Redirect to login with return URL
$login_url = wp_login_url($_SERVER['REQUEST_URI']);
wp_redirect($login_url);
exit;
}
// Show authorization consent form
$this->show_authorization_form([
'client' => $client,
'redirect_uri' => $redirect_uri,
'scope' => $scope,
'state' => $state,
'code_challenge' => $code_challenge,
'code_challenge_method' => $code_challenge_method,
]);
}
/**
* Handle token endpoint request
*/
public function handle_token_request(): void {
// Validate request method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->send_error_response(405, 'invalid_request', 'Token endpoint requires POST method');
return;
}
// Get grant type
$grant_type = sanitize_text_field($_POST['grant_type'] ?? '');
switch ($grant_type) {
case 'authorization_code':
$this->handle_authorization_code_grant();
break;
case 'refresh_token':
$this->handle_refresh_token_grant();
break;
case 'client_credentials':
$this->handle_client_credentials_grant();
break;
default:
$this->send_error_response(400, 'unsupported_grant_type', 'Unsupported grant type');
}
}
/**
* Handle authorization code grant
*/
private function handle_authorization_code_grant(): void {
// Extract parameters
$code = sanitize_text_field($_POST['code'] ?? '');
$client_id = sanitize_text_field($_POST['client_id'] ?? '');
$redirect_uri = esc_url_raw($_POST['redirect_uri'] ?? '');
$code_verifier = sanitize_text_field($_POST['code_verifier'] ?? '');
// Get client credentials from Authorization header or POST
$client_credentials = $this->extract_client_credentials();
if ($client_credentials) {
$client_id = $client_credentials['client_id'];
$client_secret = $client_credentials['client_secret'];
} else {
$client_secret = sanitize_text_field($_POST['client_secret'] ?? '');
}
// Validate required parameters
if (empty($code) || empty($client_id)) {
$this->send_error_response(400, 'invalid_request', 'Missing required parameters');
return;
}
// Get and validate authorization code
$auth_code = $this->get_authorization_code($code);
if (!$auth_code) {
$this->send_error_response(400, 'invalid_grant', 'Invalid authorization code');
return;
}
// Check if code has expired
if (strtotime($auth_code['expires']) < time()) {
$this->delete_authorization_code($code);
$this->send_error_response(400, 'invalid_grant', 'Authorization code expired');
return;
}
// Validate client
if ($auth_code['client_id'] !== $client_id) {
$this->send_error_response(400, 'invalid_client', 'Client mismatch');
return;
}
// Get client details
$client = $this->get_client($client_id);
if (!$client) {
$this->send_error_response(400, 'invalid_client', 'Invalid client');
return;
}
// Validate client secret for confidential clients
if (!$client['is_public']) {
if (empty($client_secret) || !hash_equals($client['client_secret'], $client_secret)) {
$this->send_error_response(401, 'invalid_client', 'Invalid client credentials');
return;
}
}
// Validate redirect URI
if (!empty($redirect_uri) && $auth_code['redirect_uri'] !== $redirect_uri) {
$this->send_error_response(400, 'invalid_grant', 'Redirect URI mismatch');
return;
}
// Validate PKCE for public clients or if code challenge was provided
if (!empty($auth_code['code_challenge'])) {
if (empty($code_verifier)) {
$this->send_error_response(400, 'invalid_request', 'Code verifier required');
return;
}
if (!$this->verify_pkce($code_verifier, $auth_code['code_challenge'], $auth_code['code_challenge_method'])) {
$this->send_error_response(400, 'invalid_grant', 'Invalid code verifier');
return;
}
}
// Generate tokens
$access_token = $this->generate_access_token($client_id, $auth_code['user_id'], $auth_code['scope']);
$refresh_token = $this->generate_refresh_token($client_id, $auth_code['user_id'], $auth_code['scope']);
// Delete authorization code (one-time use)
$this->delete_authorization_code($code);
// Send token response
$response = [
'access_token' => $access_token,
'token_type' => 'Bearer',
'expires_in' => $this->settings['access_token_lifetime'] ?? 3600,
'refresh_token' => $refresh_token,
'scope' => $auth_code['scope'],
];
$this->send_json_response($response);
}
/**
* Generate authorization code
*/
public function generate_authorization_code(array $params): string {
$code = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + 600); // 10 minutes
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth2_authorization_codes',
[
'authorization_code' => $code,
'client_id' => $params['client_id'],
'user_id' => get_current_user_id(),
'redirect_uri' => $params['redirect_uri'],
'expires' => $expires,
'scope' => $params['scope'] ?? '',
'code_challenge' => $params['code_challenge'] ?? '',
'code_challenge_method' => $params['code_challenge_method'] ?? '',
]
);
return $code;
}
/**
* Get authorization code from database
*/
private function get_authorization_code(string $code): ?array {
$result = $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->wpdb->prefix}oauth2_authorization_codes WHERE authorization_code = %s",
$code
),
ARRAY_A
);
return $result ?: null;
}
/**
* Delete authorization code
*/
private function delete_authorization_code(string $code): void {
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth2_authorization_codes',
['authorization_code' => $code]
);
}
/**
* Generate access token
*/
private function generate_access_token(string $client_id, int $user_id, string $scope): string {
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + ($this->settings['access_token_lifetime'] ?? 3600));
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth2_access_tokens',
[
'access_token' => $token,
'client_id' => $client_id,
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope,
]
);
return $token;
}
/**
* Generate refresh token
*/
private function generate_refresh_token(string $client_id, int $user_id, string $scope): string {
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + ($this->settings['refresh_token_lifetime'] ?? 86400 * 30)); // 30 days
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth2_refresh_tokens',
[
'refresh_token' => $token,
'client_id' => $client_id,
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope,
]
);
return $token;
}
/**
* Validate access token
*/
public function validate_access_token(string $token): ?array {
$result = $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->wpdb->prefix}oauth2_access_tokens
WHERE access_token = %s AND expires > NOW()",
$token
),
ARRAY_A
);
return $result ?: null;
}
/**
* Get client by client_id
*/
private function get_client(string $client_id): ?array {
// Query WordPress posts to find OAuth2 client
$posts = get_posts([
'post_type' => 'oauth2_client',
'meta_key' => 'client_id',
'meta_value' => $client_id,
'posts_per_page' => 1,
'post_status' => 'publish'
]);
if (empty($posts)) {
return null;
}
$post = $posts[0];
$client_data = [
'client_id' => get_post_meta($post->ID, 'client_id', true),
'client_secret' => get_post_meta($post->ID, 'client_secret', true),
'redirect_uris' => get_post_meta($post->ID, 'redirect_uris', true),
'grant_types' => get_post_meta($post->ID, 'grant_types', true),
'scope' => get_post_meta($post->ID, 'scope', true),
'is_public' => (bool)get_post_meta($post->ID, 'is_public', true),
'name' => $post->post_title,
'client_name' => $post->post_title // Add client_name for display
];
return $client_data;
}
/**
* Validate redirect URI
*/
private function validate_redirect_uri(array $client, string $redirect_uri): bool {
$redirect_uris = $client['redirect_uris'] ?? '';
if (empty($redirect_uris)) {
return false;
}
$allowed_uris = explode(',', $redirect_uris);
return in_array($redirect_uri, array_map('trim', $allowed_uris));
}
/**
* Verify PKCE code verifier
*/
private function verify_pkce(string $code_verifier, string $code_challenge, string $method): bool {
if ($method !== 'S256') {
return false;
}
$computed_challenge = rtrim(strtr(base64_encode(hash('sha256', $code_verifier, true)), '+/', '-_'), '=');
return hash_equals($code_challenge, $computed_challenge);
}
/**
* Extract client credentials from Authorization header
*/
private function extract_client_credentials(): ?array {
$auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (strpos($auth_header, 'Basic ') === 0) {
$credentials = base64_decode(substr($auth_header, 6));
$parts = explode(':', $credentials, 2);
if (count($parts) === 2) {
return [
'client_id' => $parts[0],
'client_secret' => $parts[1],
];
}
}
return null;
}
/**
* Show authorization consent form
*/
private function show_authorization_form(array $params): void {
// Set content type
header('Content-Type: text/html; charset=utf-8');
// Simple authorization form
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php _e('Authorize Application', 'wp-oauth2-poc'); ?></title>
<?php wp_head(); ?>
</head>
<body>
<div style="max-width: 500px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px;">
<h2><?php _e('Authorize Application', 'wp-oauth2-poc'); ?></h2>
<p><?php printf(__('The application "%s" wants to access your account.', 'wp-oauth2-poc'), esc_html($params['client']['client_name'])); ?></p>
<?php if (!empty($params['scope'])): ?>
<p><strong><?php _e('Requested permissions:', 'wp-oauth2-poc'); ?></strong></p>
<ul>
<?php foreach (explode(' ', $params['scope']) as $scope): ?>
<li><?php echo esc_html($scope); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<form method="post" action="<?php echo esc_url(home_url('oauth/authorize')); ?>">
<?php wp_nonce_field('oauth2_authorize', 'oauth2_nonce'); ?>
<input type="hidden" name="client_id" value="<?php echo esc_attr($params['client']['client_id']); ?>">
<input type="hidden" name="redirect_uri" value="<?php echo esc_attr($params['redirect_uri']); ?>">
<input type="hidden" name="scope" value="<?php echo esc_attr($params['scope']); ?>">
<input type="hidden" name="state" value="<?php echo esc_attr($params['state']); ?>">
<input type="hidden" name="code_challenge" value="<?php echo esc_attr($params['code_challenge']); ?>">
<input type="hidden" name="code_challenge_method" value="<?php echo esc_attr($params['code_challenge_method']); ?>">
<p>
<button type="submit" name="authorize" value="yes" class="button button-primary">
<?php _e('Authorize', 'wp-oauth2-poc'); ?>
</button>
<button type="submit" name="authorize" value="no" class="button">
<?php _e('Deny', 'wp-oauth2-poc'); ?>
</button>
</p>
</form>
</div>
<?php wp_footer(); ?>
</body>
</html>
<?php
exit;
}
/**
* Process authorization form submission
*/
public function process_authorization_form(): void {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
return;
}
// Verify nonce
if (!wp_verify_nonce($_POST['oauth2_nonce'] ?? '', 'oauth2_authorize')) {
wp_die('Security check failed');
}
// Check if user denied authorization
if (($_POST['authorize'] ?? '') !== 'yes') {
$redirect_uri = esc_url_raw($_POST['redirect_uri'] ?? '');
$state = sanitize_text_field($_POST['state'] ?? '');
$this->send_authorization_error($redirect_uri, 'access_denied', 'User denied authorization', $state);
return;
}
// Generate authorization code
$code = $this->generate_authorization_code([
'client_id' => sanitize_text_field($_POST['client_id'] ?? ''),
'redirect_uri' => esc_url_raw($_POST['redirect_uri'] ?? ''),
'scope' => sanitize_text_field($_POST['scope'] ?? ''),
'code_challenge' => sanitize_text_field($_POST['code_challenge'] ?? ''),
'code_challenge_method' => sanitize_text_field($_POST['code_challenge_method'] ?? ''),
]);
// Redirect back to client with authorization code
$redirect_uri = esc_url_raw($_POST['redirect_uri'] ?? '');
$state = sanitize_text_field($_POST['state'] ?? '');
$params = [
'code' => $code,
];
if (!empty($state)) {
$params['state'] = $state;
}
$redirect_url = add_query_arg($params, $redirect_uri);
wp_redirect($redirect_url);
exit;
}
/**
* Send authorization error response
*/
private function send_authorization_error(string $redirect_uri, string $error, string $description, string $state = ''): void {
$params = [
'error' => $error,
'error_description' => $description,
];
if (!empty($state)) {
$params['state'] = $state;
}
$redirect_url = add_query_arg($params, $redirect_uri);
wp_redirect($redirect_url);
exit;
}
/**
* Send JSON response
*/
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 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);
}
/**
* Handle refresh token grant (placeholder)
*/
private function handle_refresh_token_grant(): void {
$this->send_error_response(501, 'not_implemented', 'Refresh token grant not yet implemented');
}
/**
* Handle client credentials grant (placeholder)
*/
private function handle_client_credentials_grant(): void {
$this->send_error_response(501, 'not_implemented', 'Client credentials grant not yet implemented');
}
}

117
README.md Normal file
View File

@ -0,0 +1,117 @@
# WP OAuth2 Server
A WordPress OAuth2 authorization server implementation with PSR-4 autoloading and modular architecture.
## Overview
This is a complete OAuth2 authorization server for WordPress that transforms WordPress into an OAuth2 provider, allowing other applications to authenticate users and access WordPress resources via standard OAuth2 flows.
## Architecture
### Directory Structure
```
WPOAuth2Server/
├── Admin/ # WordPress admin interface components
├── Auth/ # Authentication mechanisms (Bearer, JWT, etc.)
├── Client/ # OAuth2 client management
├── Core/ # Core OAuth2 server implementation
├── Storage/ # Data storage adapters
└── autoloader.php # PSR-4 autoloader
```
### Key Components
- **Core/OAuth2Server.php** - Main OAuth2 server implementation
- **Core/OAuth2PoC.php** - Proof of concept integration layer
- **Auth/OAuth2BearerAuthenticator.php** - Bearer token authentication
- **Client/OAuth2ClientManager.php** - OAuth2 client management
- **Storage/** - WordPress database integration adapters
## Features
✅ **OAuth2 Authorization Code Flow**
- Complete authorization endpoint with user consent
- Token exchange with access and refresh tokens
- PKCE support for public clients
✅ **WordPress Integration**
- Seamless integration with WordPress authentication
- WordPress REST API authentication via Bearer tokens
- Custom post types for OAuth2 client storage
✅ **Security Features**
- Client credential validation
- Token expiration and refresh
- Redirect URI validation
- Scope-based access control
## Usage
### PSR-4 Autoloading
```php
require_once 'autoloader.php';
use WPOAuth2Server\Core\OAuth2Server;
use WPOAuth2Server\Core\OAuth2PoC;
// Initialize OAuth2 server
$oauth2_poc = OAuth2PoC::instance();
```
### OAuth2 Endpoints
- `/oauth/authorize` - Authorization endpoint
- `/oauth/token` - Token endpoint
- `/oauth/introspect` - Token introspection
- `/oauth/revoke` - Token revocation
### Example OAuth2 Flow
1. **Authorization Request**
```
GET /oauth/authorize?response_type=code&client_id=dev-client&redirect_uri=https://example.com/callback&scope=basic&state=xyz123
```
2. **Token Exchange**
```bash
curl -X POST /oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "redirect_uri=REDIRECT_URI"
```
3. **API Access**
```bash
curl -H "Authorization: Bearer ACCESS_TOKEN" /wp-json/wp/v2/users/me
```
## Development
### Testing
The OAuth2 server has been successfully tested with:
- Authorization code flow
- Bearer token authentication
- WordPress REST API integration
- Client credential validation
### Requirements
- PHP 7.4+
- WordPress 5.0+
- PSR-4 autoloading support
## Security Considerations
- Client secrets should be stored securely
- HTTPS should be used in production
- Token lifetimes should be configured appropriately
- Scope permissions should be carefully managed
## License
This project is part of the WordPress OAuth2 Provider plugin.

83
autoloader.php Normal file
View File

@ -0,0 +1,83 @@
<?php
/**
* WP OAuth Server CE - Autoloader
*
* PSR-4 compatible autoloader for the WP OAuth Server CE plugin.
*
* @package WPOAuth2Server
* @version 1.0.0
*/
defined('ABSPATH') or die('Direct access not allowed');
/**
* WP OAuth Server CE Autoloader
*/
class WPOAuth2Server_Autoloader {
private static $instance = null;
private $namespace_prefix = 'WPOAuth2Server\\';
private $base_directory;
/**
* Get singleton instance
*/
public static function instance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initialize autoloader
*/
private function __construct() {
$this->base_directory = __DIR__ . '/';
spl_autoload_register([$this, 'autoload']);
}
/**
* PSR-4 autoloader implementation
*
* @param string $class The fully-qualified class name
*/
public function autoload($class) {
// Check if the class uses our namespace
$len = strlen($this->namespace_prefix);
if (strncmp($this->namespace_prefix, $class, $len) !== 0) {
return; // Not our namespace, let other autoloaders handle it
}
// Get the relative class name
$relative_class = substr($class, $len);
// Replace namespace separators with directory separators
$file = $this->base_directory . str_replace('\\', '/', $relative_class) . '.php';
// If the file exists, require it
if (file_exists($file)) {
require_once $file;
}
}
/**
* Register additional namespace mapping
*
* @param string $namespace_prefix The namespace prefix
* @param string $base_directory The base directory for this namespace
*/
public function add_namespace($namespace_prefix, $base_directory) {
// Normalize namespace prefix
$namespace_prefix = trim($namespace_prefix, '\\') . '\\';
// Normalize base directory
$base_directory = rtrim($base_directory, '/\\') . '/';
// Store the mapping (for future extension)
$this->namespace_mappings[$namespace_prefix] = $base_directory;
}
}
// Initialize the autoloader
WPOAuth2Server_Autoloader::instance();