tigerstyle-scent/Client/OAuth2ClientManager.php
Ryan Malloy 120f0b616d Add release tooling and update for v1.0.0 release
- Add .distignore (operator-private files excluded)
- Add build.sh for WordPress-installable release ZIPs
- Update CLAUDE.md references (now operator-private only)
2026-05-27 14:32:07 -06:00

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