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:
commit
1da0acd25a
184
Admin/class-wo-table.php
Normal file
184
Admin/class-wo-table.php
Normal 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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
Auth/ApiKeyAuthenticator.php
Normal file
193
Auth/ApiKeyAuthenticator.php
Normal 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'];
|
||||
});
|
||||
}
|
||||
}
|
||||
75
Auth/AuthenticatorInterface.php
Normal file
75
Auth/AuthenticatorInterface.php
Normal 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
253
Auth/JwtAuthenticator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
259
Auth/OAuth2BearerAuthenticator.php
Normal file
259
Auth/OAuth2BearerAuthenticator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
304
Client/OAuth2ClientManager.php
Normal file
304
Client/OAuth2ClientManager.php
Normal 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
566
Core/OAuth2PoC.php
Normal 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
657
Core/OAuth2Server.php
Normal 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
117
README.md
Normal 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
83
autoloader.php
Normal 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();
|
||||
Loading…
x
Reference in New Issue
Block a user