- Return standard 'Bearer' token_type for OAuth2 compliance
- Add TigerStyle extensions: tigerstyle_token_type='ScentBearer'
- Support BOTH Bearer AND ScentBearer headers (dual compatibility!)
- Add scent_strength metadata for cat-themed clients
- Update README with dual authentication examples
- Maintain full OAuth2 spec compliance while keeping tiger awesomeness
🐯 Best of both worlds - standards compliance + cat magic!
552 lines
20 KiB
PHP
552 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' => '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;
|
|
}
|
|
|
|
/**
|
|
* 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...
|
|
}
|