tigerstyle-scent/includes/modules/class-scent-authenticator.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

205 lines
6.8 KiB
PHP

<?php
/**
* TigerStyle Scent Authenticator
* Implements scent-based authentication for WordPress like cats recognize each other
*
* @package TigerStyle Scent
*/
defined('ABSPATH') or die('Direct access forbidden.');
class TigerStyleScent_ScentAuthenticator implements TigerStyleScent_AuthenticatorInterface {
/**
* Authenticate user via scent token detection
*
* @return int|false User ID if scent is recognized, false otherwise
*/
public function authenticate() {
// Detect scent token from HTTP headers
$scent_token = $this->detect_scent_token();
if (!$scent_token) {
return false;
}
// Validate scent and return associated user ID
return $this->validate_scent_token($scent_token);
}
/**
* Detect scent token from various sources like a cat's keen senses
*
* @return string|null The detected scent token
*/
private function detect_scent_token(): ?string {
// Check Authorization header for Bearer/ScentBearer token
$auth_header = $this->get_authorization_header();
if ($auth_header) {
// Support both Bearer and our custom ScentBearer format
if (preg_match('/^(?:Bearer|ScentBearer)\s+(.+)$/i', $auth_header, $matches)) {
return trim($matches[1]);
}
}
// Check for scent token in POST data (like a scent trail)
if (isset($_POST['scent_token'])) {
return sanitize_text_field($_POST['scent_token']);
}
// Check for scent token in GET parameters (less secure, like faint scent)
if (isset($_GET['scent_token'])) {
return sanitize_text_field($_GET['scent_token']);
}
return null;
}
/**
* Get Authorization header across different server configurations
*
* @return string|null
*/
private function get_authorization_header(): ?string {
$auth_header = null;
// Standard HTTP_AUTHORIZATION header
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$auth_header = $_SERVER['HTTP_AUTHORIZATION'];
}
// Alternative header names used by some servers
elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$auth_header = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
}
// Check for Authorization header in apache_request_headers()
elseif (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (isset($headers['Authorization'])) {
$auth_header = $headers['Authorization'];
}
}
// 🔐 SECURITY: Validate authorization header format to prevent injection
if ($auth_header !== null) {
// Only allow Bearer/ScentBearer tokens with valid characters
if (preg_match('/^(Bearer|ScentBearer)\s+([A-Za-z0-9+\/=._-]+)$/i', $auth_header, $matches)) {
return $auth_header;
}
// Log suspicious authorization header attempts
if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) {
error_log('[TigerStyle Scent Security] Invalid authorization header format: ' . substr($auth_header, 0, 50));
}
return null;
}
return null;
}
/**
* Validate scent token and return associated user ID like recognizing a familiar cat
*/
private function validate_scent_token(string $token): ?int {
// Use ScentServer for database-backed token validation
$scent_server = new TigerStyleScent_ScentServer([]);
$scent_data = $scent_server->analyze_scent_token($token);
if ($scent_data && $scent_data['active']) {
// Log successful scent recognition
do_action('tigerstyle_scent_authenticated', $scent_data['user_id'], $scent_data['client_id']);
return $scent_data['user_id'];
}
// Log failed scent recognition attempt
do_action('tigerstyle_scent_authentication_failed', $token);
return null;
}
/**
* Get authentication type identifier
*
* @return string
*/
public function get_type(): string {
return 'scent_token';
}
/**
* Get authentication priority (higher = checked first)
* Scent authentication should be high priority like a cat's primary sense
*
* @return int
*/
public function get_priority(): int {
return 20; // Higher than basic auth, lower than emergency authentication
}
/**
* Check if this authenticator can handle the current request
* Like a cat detecting if there's a scent to analyze
*
* @return bool
*/
public function can_handle_request(): bool {
// Can handle if we detect any scent token
return $this->detect_scent_token() !== null;
}
/**
* Provide authentication challenges/hints for API clients
*
* @return array
*/
public function get_authentication_challenge(): array {
return [
'type' => 'ScentBearer',
'realm' => 'TigerStyle Territory',
'description' => 'Provide your scent token in the Authorization header: "ScentBearer YOUR_TOKEN"',
'hint' => 'Get your scent token from /oauth/token endpoint'
];
}
/**
* Verify scent token scope for specific resource access
* Like checking if a cat has permission to enter certain territory
*
* @param string $token Scent token to verify
* @param string $required_scope Required scope for access
* @return bool
*/
public function verify_territory_access(string $token, string $required_scope): bool {
$scent_server = new TigerStyleScent_ScentServer([]);
$scent_data = $scent_server->analyze_scent_token($token);
if (!$scent_data || !$scent_data['active']) {
return false;
}
// Check if token has required scope
$token_scopes = explode(' ', $scent_data['scope'] ?? '');
return in_array($required_scope, $token_scopes);
}
/**
* Log scent authentication events for territory monitoring
*
* @param string $event Event type
* @param array $data Event data
*/
private function log_scent_event(string $event, array $data = []): void {
if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) {
error_log(sprintf(
'[TigerStyle Scent] %s: %s',
$event,
json_encode($data)
));
}
// Fire WordPress action for logging systems
do_action('tigerstyle_scent_log', $event, $data);
}
}