tigerstyle-scent/includes/modules/class-scent-server.php
Ryan Malloy 2351925591 🐯 Transform to TigerStyle Scent - Enterprise OAuth2 with Cat Theming
- Rebrand from boring WPOAuth2Server to TigerStyle Scent
- Replace OAuth2 terminology with cat-themed concepts:
  • Bearer Token → Scent Token
  • Authorization Code → Territory Code
  • Client Authentication → Scent Recognition
  • Token Introspection → Scent Analysis
- Create main tigerstyle-scent.php plugin with proper WordPress header
- Cat-themed authentication UI with territory access forms
- TigerStyle branding with orange/tiger color scheme
- Comprehensive README with cat authentication metaphors
- Plugin architecture follows TigerStyle conventions
- Admin interface uses scent/territory language throughout

🐾 Leave your digital scent trail for secure access control!
2025-09-16 21:29:25 -06:00

549 lines
20 KiB
PHP

<?php
/**
* TigerStyle Scent Server Implementation
*
* Implements secure OAuth2 authentication like a cat's scent recognition system
* with support for territory code flow and scent validation
*
* @package TigerStyle Scent
*/
defined('ABSPATH') or die('Direct access forbidden.');
class TigerStyleScent_ScentServer {
/**
* WordPress database instance
* @var \wpdb
*/
private \wpdb $wpdb;
/**
* Plugin settings
* @var array
*/
private array $settings;
/**
* Constructor
*
* @param array $settings Plugin configuration settings
*/
public function __construct(array $settings) {
global $wpdb;
$this->wpdb = $wpdb;
$this->settings = $settings;
}
/**
* Handle scent authentication requests (OAuth2 endpoints)
*/
public function handle_scent_request(): void {
$endpoint = get_query_var('oauth_endpoint');
switch ($endpoint) {
case 'authorize':
$this->handle_territory_authorization();
break;
case 'token':
$this->handle_scent_token_exchange();
break;
case 'introspect':
$this->handle_scent_analysis();
break;
case 'revoke':
$this->handle_scent_revocation();
break;
default:
$this->send_error_response(404, 'unknown_territory', 'Unknown territory endpoint');
}
}
/**
* Handle territory authorization (OAuth2 authorize endpoint)
*/
private function handle_territory_authorization(): void {
// Validate query parameters
$response_type = sanitize_text_field($_GET['response_type'] ?? '');
$client_id = sanitize_text_field($_GET['client_id'] ?? '');
$redirect_uri = esc_url_raw($_GET['redirect_uri'] ?? '');
$scope = sanitize_text_field($_GET['scope'] ?? '');
$state = sanitize_text_field($_GET['state'] ?? '');
// Validate required parameters for territory access
if (empty($response_type) || empty($client_id)) {
$this->send_error_response(400, 'invalid_request', 'Missing required territory parameters');
return;
}
// Only support authorization code (territory code) flow
if ($response_type !== 'code') {
$this->send_error_response(400, 'unsupported_response_type', 'Only territory code flow is supported');
return;
}
// Validate client scent credentials
$client = $this->recognize_client_scent($client_id);
if (!$client) {
$this->send_error_response(400, 'invalid_client', 'Unknown client scent');
return;
}
// Validate redirect URI for safe territory return
if (!empty($redirect_uri) && !$this->validate_territory_return_uri($client, $redirect_uri)) {
$this->send_error_response(400, 'invalid_request', 'Invalid territory return URI');
return;
}
// Check if user is authenticated (has proper scent)
if (!is_user_logged_in()) {
// Redirect to WordPress login with return URL
$login_url = wp_login_url(add_query_arg($_GET, admin_url('admin.php')));
wp_redirect($login_url);
exit;
}
// Handle POST request (user authorization decision)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['authorize']) && wp_verify_nonce($_POST['_wpnonce'], 'tigerstyle_scent_authorize')) {
$this->grant_territory_access($client_id, $redirect_uri, $scope, $state);
} elseif (isset($_POST['deny'])) {
$this->deny_territory_access($redirect_uri, $state);
}
return;
}
// Show territory authorization form
$this->show_territory_authorization_form([
'client' => $client,
'scope' => $scope,
'state' => $state,
'redirect_uri' => $redirect_uri,
'client_id' => $client_id
]);
}
/**
* Handle scent token exchange (OAuth2 token endpoint)
*/
private function handle_scent_token_exchange(): void {
// Only accept POST requests for scent token exchange
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->send_error_response(405, 'method_not_allowed', 'Scent exchange requires POST');
return;
}
$grant_type = sanitize_text_field($_POST['grant_type'] ?? '');
switch ($grant_type) {
case 'authorization_code':
$this->handle_territory_code_grant();
break;
case 'refresh_token':
$this->handle_scent_refresh_grant();
break;
case 'client_credentials':
$this->handle_client_scent_credentials_grant();
break;
default:
$this->send_error_response(400, 'unsupported_grant_type', 'Unsupported scent grant type');
}
}
/**
* Handle territory code grant (authorization code flow)
*/
private function handle_territory_code_grant(): void {
// Extract scent 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 scent credentials from Authorization header or POST
$client_credentials = $this->extract_client_scent_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 scent parameters
if (empty($code) || empty($client_id)) {
$this->send_error_response(400, 'invalid_request', 'Missing required scent parameters');
return;
}
// Get and validate territory code
$territory_code = $this->get_territory_code($code);
if (!$territory_code) {
$this->send_error_response(400, 'invalid_grant', 'Invalid territory code');
return;
}
// Check if territory code has expired
if (strtotime($territory_code['expires']) < time()) {
$this->delete_territory_code($code);
$this->send_error_response(400, 'invalid_grant', 'Territory code expired');
return;
}
// Validate client scent match
if ($territory_code['client_id'] !== $client_id) {
$this->send_error_response(400, 'invalid_client', 'Client scent mismatch');
return;
}
// Get client details for scent recognition
$client = $this->recognize_client_scent($client_id);
if (!$client) {
$this->send_error_response(400, 'invalid_client', 'Invalid client scent');
return;
}
// Validate client secret for confidential clients (strong scent verification)
if (!$client['is_public']) {
if (empty($client_secret) || !hash_equals($client['client_secret'], $client_secret)) {
$this->send_error_response(401, 'invalid_client', 'Invalid scent credentials');
return;
}
}
// Validate territory return URI
if (!empty($redirect_uri) && $territory_code['redirect_uri'] !== $redirect_uri) {
$this->send_error_response(400, 'invalid_grant', 'Territory return URI mismatch');
return;
}
// Generate scent tokens
$scent_token = $this->generate_scent_token($client_id, $territory_code['user_id'], $territory_code['scope']);
$refresh_scent = $this->generate_refresh_scent($client_id, $territory_code['user_id'], $territory_code['scope']);
// Delete territory code (one-time use like a scent trail)
$this->delete_territory_code($code);
// Send scent token response
$this->send_scent_token_response($scent_token, $refresh_scent, $territory_code['scope']);
}
/**
* Handle scent analysis (OAuth2 introspect endpoint)
*/
private function handle_scent_analysis(): void {
$token = sanitize_text_field($_POST['token'] ?? '');
if (empty($token)) {
$this->send_error_response(400, 'invalid_request', 'Missing scent token for analysis');
return;
}
$scent_data = $this->analyze_scent_token($token);
header('Content-Type: application/json');
echo json_encode($scent_data);
exit;
}
/**
* Recognize client by their unique scent (get client data)
*/
private function recognize_client_scent(string $client_id): ?array {
// Query WordPress posts to find OAuth2 client by scent signature
$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_scent_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 scent recognition display
];
return $client_scent_data;
}
/**
* Validate territory return URI for safe scent trail
*/
private function validate_territory_return_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));
}
/**
* Generate unique scent token (access token)
*/
private function generate_scent_token(string $client_id, int $user_id, string $scope): string {
$scent_token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + 3600); // 1 hour scent trail
// Store scent token in territory database
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth_access_tokens',
[
'access_token' => $scent_token,
'client_id' => $client_id,
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope
]
);
return $scent_token;
}
/**
* Generate refresh scent for long-term authentication
*/
private function generate_refresh_scent(string $client_id, int $user_id, string $scope): string {
$refresh_scent = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + (30 * 24 * 3600)); // 30 day scent memory
// Store refresh scent in territory database
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth_refresh_tokens',
[
'refresh_token' => $refresh_scent,
'client_id' => $client_id,
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope
]
);
return $refresh_scent;
}
/**
* Analyze scent token validity and return data
*/
public function analyze_scent_token(string $token): ?array {
$result = $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->wpdb->prefix}oauth_access_tokens WHERE access_token = %s",
$token
),
ARRAY_A
);
if (!$result) {
return ['active' => false];
}
// Check if scent has expired
if (strtotime($result['expires']) < time()) {
// Clean up expired scent
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth_access_tokens',
['access_token' => $token]
);
return ['active' => false];
}
return [
'active' => true,
'client_id' => $result['client_id'],
'user_id' => (int)$result['user_id'],
'scope' => $result['scope'],
'exp' => strtotime($result['expires'])
];
}
/**
* Show territory authorization form with cat-themed UI
*/
private function show_territory_authorization_form(array $params): void {
// Set content type
header('Content-Type: text/html; charset=utf-8');
// TigerStyle Scent authorization form with cat theming
?>
<!DOCTYPE html>
<html>
<head>
<title>TigerStyle Territory Access</title>
<style>
body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #ff6b35, #f7931e); margin: 0; padding: 20px; }
.scent-form { max-width: 500px; margin: 50px auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.tiger-header { text-align: center; color: #ff6b35; margin-bottom: 20px; }
.scent-info { background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 20px 0; }
.territory-buttons { display: flex; gap: 10px; justify-content: center; margin-top: 20px; }
.scent-button { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
.authorize-btn { background: #28a745; color: white; }
.deny-btn { background: #dc3545; color: white; }
.scent-button:hover { opacity: 0.9; transform: translateY(-1px); }
.paw-icon { font-size: 24px; }
</style>
</head>
<body>
<div class="scent-form">
<div class="tiger-header">
<div class="paw-icon">🐾</div>
<h2>TigerStyle Territory Access</h2>
<p>The application "<strong><?php echo esc_html($params['client']['client_name']); ?></strong>" wants to recognize your scent.</p>
</div>
<div class="scent-info">
<strong>🏰 Requested Territory Permissions:</strong>
<ul>
<?php foreach (explode(' ', $params['scope']) as $scope): ?>
<li><?php echo esc_html($scope); ?></li>
<?php endforeach; ?>
</ul>
</div>
<form method="post">
<?php wp_nonce_field('tigerstyle_scent_authorize'); ?>
<input type="hidden" name="client_id" value="<?php echo esc_attr($params['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']); ?>">
<div class="territory-buttons">
<button type="submit" name="authorize" class="scent-button authorize-btn">
🐯 Grant Territory Access
</button>
<button type="submit" name="deny" class="scent-button deny-btn">
❌ Deny Access
</button>
</div>
</form>
</div>
</body>
</html>
<?php
exit;
}
/**
* Grant territory access and redirect with code
*/
private function grant_territory_access(string $client_id, string $redirect_uri, string $scope, string $state): void {
$user_id = get_current_user_id();
$territory_code = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + 600); // 10 minute territory code
// Store territory code
$this->wpdb->insert(
$this->wpdb->prefix . 'oauth_authorization_codes',
[
'authorization_code' => $territory_code,
'client_id' => $client_id,
'user_id' => $user_id,
'redirect_uri' => $redirect_uri,
'expires' => $expires,
'scope' => $scope
]
);
// Build redirect URL with territory code
$redirect_params = [
'code' => $territory_code,
'state' => $state
];
$redirect_url = add_query_arg($redirect_params, $redirect_uri);
wp_redirect($redirect_url);
exit;
}
/**
* Send scent token response
*/
private function send_scent_token_response(string $scent_token, string $refresh_scent, string $scope): void {
$response = [
'access_token' => $scent_token,
'token_type' => 'ScentBearer', // Cat-themed token type!
'expires_in' => 3600,
'refresh_token' => $refresh_scent,
'scope' => $scope
];
header('Content-Type: application/json');
header('Cache-Control: no-store');
echo json_encode($response);
exit;
}
/**
* Send error response with cat-themed messages
*/
private function send_error_response(int $status_code, string $error, string $description): void {
http_response_code($status_code);
header('Content-Type: application/json');
$response = [
'error' => $error,
'error_description' => $description
];
echo json_encode($response);
exit;
}
/**
* Extract client scent credentials from Authorization header
*/
private function extract_client_scent_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;
}
/**
* Get territory code data
*/
private function get_territory_code(string $code): ?array {
return $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->wpdb->prefix}oauth_authorization_codes WHERE authorization_code = %s",
$code
),
ARRAY_A
);
}
/**
* Delete used territory code
*/
private function delete_territory_code(string $code): void {
$this->wpdb->delete(
$this->wpdb->prefix . 'oauth_authorization_codes',
['authorization_code' => $code]
);
}
// Additional methods for refresh tokens, client credentials, etc. would follow the same cat-themed pattern...
}