- Add .distignore (operator-private files excluded) - Add build.sh for WordPress-installable release ZIPs - Update CLAUDE.md references (now operator-private only)
304 lines
9.5 KiB
PHP
304 lines
9.5 KiB
PHP
<?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' => password_hash($client_secret, PASSWORD_ARGON2ID), // 🔐 SECURITY: Use proper password hashing
|
|
'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;
|
|
}
|
|
} |