- Add .distignore (operator-private files excluded) - Add build.sh for WordPress-installable release ZIPs - Update CLAUDE.md references (now operator-private only)
987 lines
36 KiB
PHP
987 lines
36 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');
|
|
|
|
// 🔐 SECURITY: Enforce HTTPS for all OAuth2 endpoints
|
|
$this->enforce_https();
|
|
|
|
// 🔐 SECURITY: Add comprehensive security headers
|
|
$this->add_security_headers();
|
|
|
|
// 🔐 SECURITY: Check for blocked IPs and emergency blocks
|
|
if ($this->is_client_blocked()) {
|
|
$this->send_blocked_response();
|
|
return;
|
|
}
|
|
|
|
// 🔐 SECURITY: Validate request integrity
|
|
if (!$this->validate_request_integrity()) {
|
|
$this->log_security_violation('Request integrity validation failed');
|
|
$this->send_error_response(400, 'invalid_request', 'Request validation failed');
|
|
return;
|
|
}
|
|
|
|
// 🔐 SECURITY: Check rate limits before processing request
|
|
if (!TigerStyleScent_RateLimiter::check_rate_limit($endpoint)) {
|
|
TigerStyleScent_RateLimiter::send_rate_limit_response($endpoint);
|
|
return;
|
|
}
|
|
|
|
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 {
|
|
// 🔐 SECURITY: Comprehensive OAuth2 parameter validation
|
|
$validation_rules = TigerStyleScent_InputValidator::get_oauth2_validation_rules();
|
|
$auth_rules = [
|
|
'response_type' => $validation_rules['response_type'],
|
|
'client_id' => $validation_rules['client_id'],
|
|
'redirect_uri' => $validation_rules['redirect_uri'],
|
|
'scope' => $validation_rules['scope'],
|
|
'state' => $validation_rules['state']
|
|
];
|
|
|
|
$validation_result = TigerStyleScent_InputValidator::validate_oauth2_request($_GET, $auth_rules);
|
|
|
|
if (!$validation_result['valid']) {
|
|
// Log validation failure with detailed context
|
|
TigerStyleScent_SecurityLogger::log_security_event(
|
|
TigerStyleScent_SecurityLogger::EVENT_VALIDATION_FAILURE,
|
|
TigerStyleScent_SecurityLogger::SEVERITY_MEDIUM,
|
|
'Authorization endpoint validation failed',
|
|
[
|
|
'errors' => $validation_result['errors'],
|
|
'warnings' => $validation_result['warnings'],
|
|
'raw_input' => $_GET
|
|
]
|
|
);
|
|
|
|
$this->send_error_response(400, 'invalid_request', 'Invalid request parameters');
|
|
return;
|
|
}
|
|
|
|
// Extract validated parameters
|
|
$response_type = $validation_result['sanitized']['response_type'];
|
|
$client_id = $validation_result['sanitized']['client_id'];
|
|
$redirect_uri = $validation_result['sanitized']['redirect_uri'];
|
|
$scope = $validation_result['sanitized']['scope'] ?? 'basic';
|
|
$state = $validation_result['sanitized']['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) {
|
|
// 🔐 SECURITY: Log client authentication failure
|
|
TigerStyleScent_SecurityLogger::log_security_event(
|
|
TigerStyleScent_SecurityLogger::EVENT_AUTH_FAILURE,
|
|
TigerStyleScent_SecurityLogger::SEVERITY_MEDIUM,
|
|
'Unknown client attempted authentication',
|
|
['client_id' => $client_id, 'endpoint' => 'token'],
|
|
$client_id
|
|
);
|
|
|
|
$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) || !password_verify($client_secret, $client['client_secret'])) {
|
|
// 🔐 SECURITY: Log client secret failure
|
|
TigerStyleScent_SecurityLogger::log_security_event(
|
|
TigerStyleScent_SecurityLogger::EVENT_AUTH_FAILURE,
|
|
TigerStyleScent_SecurityLogger::SEVERITY_HIGH,
|
|
'Invalid client secret provided',
|
|
['client_id' => $client_id, 'endpoint' => 'token', 'is_public' => false],
|
|
$client_id
|
|
);
|
|
|
|
$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']);
|
|
|
|
// 🔐 SECURITY: Log successful token issuance
|
|
TigerStyleScent_SecurityLogger::log_security_event(
|
|
TigerStyleScent_SecurityLogger::EVENT_TOKEN_ISSUED,
|
|
TigerStyleScent_SecurityLogger::SEVERITY_LOW,
|
|
'OAuth2 tokens successfully issued',
|
|
[
|
|
'grant_type' => 'authorization_code',
|
|
'scope' => $territory_code['scope'],
|
|
'token_length' => strlen($scent_token),
|
|
'refresh_token_issued' => !empty($refresh_scent)
|
|
],
|
|
$client_id,
|
|
$territory_code['user_id']
|
|
);
|
|
|
|
// 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 {
|
|
// 🔐 CSRF Protection for POST requests
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$nonce = sanitize_text_field($_POST['_wpnonce'] ?? '');
|
|
if (!wp_verify_nonce($nonce, 'tigerstyle_scent_introspect')) {
|
|
$this->send_error_response(403, 'invalid_request', 'CSRF token required');
|
|
return;
|
|
}
|
|
}
|
|
|
|
$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 {
|
|
// 🔐 SECURITY: Maximum entropy token generation (48 bytes = 384 bits)
|
|
$scent_token = $this->generate_secure_token(48);
|
|
$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 {
|
|
// 🔐 SECURITY: Higher entropy for refresh tokens (64 bytes = 512 bits)
|
|
$refresh_scent = $this->generate_secure_token(64);
|
|
$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();
|
|
// 🔐 SECURITY: Secure authorization code generation (40 bytes = 320 bits)
|
|
$territory_code = $this->generate_secure_token(40);
|
|
$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' => 'Bearer', // OAuth2 compliant, but we detect ScentBearer too!
|
|
'expires_in' => 3600,
|
|
'refresh_token' => $refresh_scent,
|
|
'scope' => $scope,
|
|
// TigerStyle extension - clients can use either Bearer or ScentBearer
|
|
'tigerstyle_token_type' => 'ScentBearer',
|
|
'scent_strength' => 'strong' // Cat-themed metadata
|
|
];
|
|
|
|
header('Content-Type: application/json');
|
|
header('Cache-Control: no-store');
|
|
echo json_encode($response);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Sanitize error messages to prevent information disclosure
|
|
*/
|
|
private function get_safe_error_message(string $error_type): string {
|
|
$safe_messages = [
|
|
'invalid_client' => 'Authentication failed',
|
|
'invalid_grant' => 'Request denied',
|
|
'invalid_request' => 'Bad request',
|
|
'invalid_scope' => 'Access denied',
|
|
'unauthorized_client' => 'Unauthorized',
|
|
'unsupported_grant_type' => 'Request type not supported',
|
|
'unsupported_response_type' => 'Response type not supported',
|
|
'unknown_territory' => 'Endpoint not found',
|
|
'method_not_allowed' => 'Method not allowed',
|
|
'rate_limit_exceeded' => 'Too many requests'
|
|
];
|
|
|
|
return $safe_messages[$error_type] ?? 'Request failed';
|
|
}
|
|
|
|
/**
|
|
* Send error response with sanitized messages
|
|
*/
|
|
private function send_error_response(int $status_code, string $error, string $description): void {
|
|
// 🔐 SECURITY: Use sanitized error messages in production
|
|
$safe_description = $this->get_safe_error_message($error);
|
|
|
|
// In debug mode, show detailed errors for development
|
|
if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) {
|
|
$safe_description = $description;
|
|
}
|
|
|
|
http_response_code($status_code);
|
|
header('Content-Type: application/json');
|
|
|
|
$response = [
|
|
'error' => $error,
|
|
'error_description' => $safe_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]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Enforce HTTPS for all OAuth2 endpoints
|
|
*/
|
|
private function enforce_https(): void {
|
|
// Check if HTTPS is required in settings
|
|
$require_https = $this->settings['require_https'] ?? true;
|
|
|
|
if ($require_https && !is_ssl()) {
|
|
// Log security violation
|
|
if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) {
|
|
error_log('[TigerStyle Scent Security] HTTP request blocked - HTTPS required');
|
|
}
|
|
|
|
// Return secure error
|
|
http_response_code(400);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'error' => 'invalid_request',
|
|
'error_description' => 'HTTPS required for OAuth2 endpoints'
|
|
]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Add comprehensive security headers
|
|
*/
|
|
private function add_security_headers(): void {
|
|
// Prevent clickjacking
|
|
header('X-Frame-Options: DENY');
|
|
|
|
// XSS protection
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
|
|
// MIME type sniffing protection
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
// Referrer policy for privacy
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
|
|
// Content Security Policy for OAuth2 endpoints
|
|
header("Content-Security-Policy: default-src 'none'; script-src 'none'; object-src 'none'; base-uri 'none';");
|
|
|
|
// HSTS header for HTTPS enforcement (1 year)
|
|
if (is_ssl()) {
|
|
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
|
|
}
|
|
|
|
// Cache control for sensitive OAuth2 responses
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
header('Pragma: no-cache');
|
|
header('Expires: 0');
|
|
|
|
// Feature policy to disable potentially dangerous features
|
|
header('Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()');
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Generate cryptographically secure tokens with maximum entropy
|
|
*
|
|
* @param int $bytes Number of random bytes (determines entropy)
|
|
* @return string Base64URL encoded secure token
|
|
*/
|
|
private function generate_secure_token(int $bytes): string {
|
|
// Generate cryptographically secure random bytes
|
|
$random_bytes = random_bytes($bytes);
|
|
|
|
// Add additional entropy sources for maximum security
|
|
$entropy_sources = [
|
|
microtime(true),
|
|
wp_salt('auth'),
|
|
wp_salt('secure_auth'),
|
|
$_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
$_SERVER['REMOTE_ADDR'] ?? '',
|
|
wp_generate_uuid4(),
|
|
];
|
|
|
|
// Combine entropy sources
|
|
$additional_entropy = hash('sha256', json_encode($entropy_sources), true);
|
|
|
|
// Mix random bytes with additional entropy using HMAC
|
|
$mixed_entropy = hash_hmac('sha256', $random_bytes, $additional_entropy, true);
|
|
|
|
// Use base64url encoding for safe URL transmission
|
|
return rtrim(strtr(base64_encode($mixed_entropy . $random_bytes), '+/', '-_'), '=');
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Check if client IP is blocked
|
|
*/
|
|
private function is_client_blocked(): bool {
|
|
$client_ip = $this->get_client_ip();
|
|
|
|
// Check emergency blocks
|
|
$emergency_block = get_transient("tigerstyle_scent_emergency_block_{$client_ip}");
|
|
if ($emergency_block) {
|
|
return true;
|
|
}
|
|
|
|
// Check rate limit blocks
|
|
$rate_limit_block = get_transient("tigerstyle_scent_rate_block_{$client_ip}");
|
|
if ($rate_limit_block) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Send blocked response
|
|
*/
|
|
private function send_blocked_response(): void {
|
|
// Log the blocked attempt
|
|
TigerStyleScent_SecurityLogger::log_security_event(
|
|
TigerStyleScent_SecurityLogger::EVENT_SECURITY_VIOLATION,
|
|
TigerStyleScent_SecurityLogger::SEVERITY_HIGH,
|
|
'Blocked IP attempted access',
|
|
['blocked_ip' => $this->get_client_ip()]
|
|
);
|
|
|
|
http_response_code(403);
|
|
header('Content-Type: application/json');
|
|
|
|
echo json_encode([
|
|
'error' => 'access_denied',
|
|
'error_description' => 'Access temporarily restricted'
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Validate request integrity
|
|
*/
|
|
private function validate_request_integrity(): bool {
|
|
// Check request size limits
|
|
if (!$this->validate_request_size()) {
|
|
return false;
|
|
}
|
|
|
|
// Validate content type for POST requests
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$this->validate_content_type()) {
|
|
return false;
|
|
}
|
|
|
|
// Check for suspicious request patterns
|
|
if ($this->detect_suspicious_patterns()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Validate request size limits
|
|
*/
|
|
private function validate_request_size(): bool {
|
|
$max_content_length = 1024 * 1024; // 1MB limit
|
|
$content_length = $_SERVER['CONTENT_LENGTH'] ?? 0;
|
|
|
|
if ($content_length > $max_content_length) {
|
|
$this->log_security_violation('Request size exceeded limit', [
|
|
'content_length' => $content_length,
|
|
'max_allowed' => $max_content_length
|
|
]);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Validate content type for POST requests
|
|
*/
|
|
private function validate_content_type(): bool {
|
|
$content_type = $_SERVER['CONTENT_TYPE'] ?? '';
|
|
$allowed_types = [
|
|
'application/x-www-form-urlencoded',
|
|
'application/json',
|
|
'multipart/form-data'
|
|
];
|
|
|
|
foreach ($allowed_types as $type) {
|
|
if (strpos($content_type, $type) === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$this->log_security_violation('Invalid content type', [
|
|
'content_type' => $content_type,
|
|
'allowed_types' => $allowed_types
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Detect suspicious request patterns
|
|
*/
|
|
private function detect_suspicious_patterns(): bool {
|
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
|
|
// Patterns that indicate potential attacks
|
|
$suspicious_patterns = [
|
|
// Directory traversal
|
|
'/\.\.[\/\\\\]/',
|
|
// SQL injection patterns
|
|
'/union\s+select/i',
|
|
'/drop\s+table/i',
|
|
// Script injection
|
|
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i',
|
|
// Command injection
|
|
'/[;&|`$]/',
|
|
// Null bytes
|
|
'/\x00/',
|
|
];
|
|
|
|
foreach ($suspicious_patterns as $pattern) {
|
|
if (preg_match($pattern, $request_uri) || preg_match($pattern, $user_agent)) {
|
|
$this->log_security_violation('Suspicious request pattern detected', [
|
|
'pattern' => $pattern,
|
|
'request_uri' => $request_uri,
|
|
'user_agent' => substr($user_agent, 0, 255)
|
|
]);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Enhanced security headers
|
|
*/
|
|
private function add_security_headers(): void {
|
|
// Prevent clickjacking
|
|
header('X-Frame-Options: DENY');
|
|
|
|
// Prevent MIME sniffing
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
// XSS protection
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
|
|
// Referrer policy
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
|
|
// Content Security Policy for OAuth2 endpoints
|
|
header("Content-Security-Policy: default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'");
|
|
|
|
// Force HTTPS (HSTS)
|
|
if (is_ssl()) {
|
|
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
|
|
}
|
|
|
|
// Custom security header for identification
|
|
header('X-Security-Exemplar: TigerStyle-Scent-OAuth2');
|
|
|
|
// Prevent caching of sensitive OAuth2 responses
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, private');
|
|
header('Pragma: no-cache');
|
|
header('Expires: 0');
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Log security violations
|
|
*/
|
|
private function log_security_violation(string $message, array $context = []): void {
|
|
TigerStyleScent_SecurityLogger::log_security_event(
|
|
TigerStyleScent_SecurityLogger::EVENT_SECURITY_VIOLATION,
|
|
TigerStyleScent_SecurityLogger::SEVERITY_HIGH,
|
|
$message,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Get client IP with proxy support
|
|
*/
|
|
private function get_client_ip(): string {
|
|
// Check for IP from headers (in order of preference)
|
|
$ip_headers = [
|
|
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
|
'HTTP_X_FORWARDED_FOR', // General proxy
|
|
'HTTP_X_REAL_IP', // Nginx proxy
|
|
'HTTP_X_FORWARDED', // Squid proxy
|
|
'HTTP_FORWARDED_FOR', // Legacy
|
|
'HTTP_FORWARDED', // RFC 7239
|
|
'REMOTE_ADDR' // Direct connection
|
|
];
|
|
|
|
foreach ($ip_headers as $header) {
|
|
$ip = $_SERVER[$header] ?? '';
|
|
if (!empty($ip)) {
|
|
// Handle comma-separated IPs (take first one)
|
|
$ip = trim(explode(',', $ip)[0]);
|
|
|
|
// Validate IP address
|
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
|
return $ip;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to REMOTE_ADDR (may be private/local IP)
|
|
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
}
|
|
|
|
// Additional methods for refresh tokens, client credentials, etc. would follow the same cat-themed pattern...
|
|
}
|