security: Auth uses TokenManager for JWT resolution
This commit is contained in:
+35
-65
@@ -4,100 +4,70 @@ namespace AskTamMCP\Core;
|
|||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticates incoming requests.
|
|
||||||
*
|
|
||||||
* Supports two auth methods:
|
|
||||||
* 1. Bearer token (Authorization: Bearer <key>) — for Claude.ai connectors + standard OAuth clients
|
|
||||||
* 2. X-MCP-API-Key header — for direct API / OpenAI GPT Actions
|
|
||||||
*
|
|
||||||
* Both resolve to the same DB key row. Bearer is checked first.
|
|
||||||
*/
|
|
||||||
class Auth {
|
class Auth {
|
||||||
|
|
||||||
public const SCOPE_READ = 'read';
|
public const SCOPE_READ = 'read';
|
||||||
public const SCOPE_WRITE = 'write';
|
public const SCOPE_WRITE = 'write';
|
||||||
public const SCOPE_ADMIN = 'admin';
|
public const SCOPE_ADMIN = 'admin';
|
||||||
public const SCOPE_WOOCOMMERCE = 'woocommerce';
|
public const SCOPE_WOOCOMMERCE = 'woocommerce';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate request auth and check required scope.
|
* Validate request and check scope.
|
||||||
* Returns the key row on success, WP_Error on failure.
|
* Returns key row on success, WP_Error on failure.
|
||||||
*/
|
*/
|
||||||
public static function validate(\WP_REST_Request $request, string $requiredScope = self::SCOPE_READ): \WP_Error|array {
|
public static function validate(\WP_REST_Request $request, string $requiredScope = self::SCOPE_READ): \WP_Error|array {
|
||||||
$apiKey = self::extractKey($request);
|
$token = self::extractToken($request);
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($token)) {
|
||||||
return new \WP_Error(
|
return new \WP_Error('missing_auth', 'Authentication required. Provide Authorization: Bearer <token>', ['status' => 401]);
|
||||||
'missing_api_key',
|
|
||||||
'Authentication required. Provide key via: Authorization: Bearer <key> or X-MCP-API-Key: <key>',
|
|
||||||
['status' => 401]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global $wpdb;
|
// Resolve via TokenManager (JWT first, raw key fallback)
|
||||||
$table = $wpdb->prefix . 'asktam_mcp_keys';
|
$row = TokenManager::resolve($token);
|
||||||
$row = $wpdb->get_row(
|
|
||||||
$wpdb->prepare("SELECT * FROM {$table} WHERE api_key = %s AND is_active = 1", $apiKey),
|
|
||||||
ARRAY_A
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$row) {
|
if (!$row) {
|
||||||
return new \WP_Error('invalid_api_key', 'Invalid or inactive API key.', ['status' => 401]);
|
return new \WP_Error('invalid_token', 'Invalid or expired token.', ['status' => 401]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limiting — transient per key per minute
|
// Rate limit — transient per key ID per minute
|
||||||
$rlKey = 'asktam_mcp_rl_' . md5($apiKey);
|
$rlKey = 'asktam_mcp_rl_' . $row['id'];
|
||||||
$settings = get_option('asktam_mcp_settings', []);
|
$limit = (int) (get_option('asktam_mcp_settings', [])['rate_limit'] ?? 60);
|
||||||
$limit = (int) ($settings['rate_limit'] ?? 60);
|
$count = (int) get_transient($rlKey);
|
||||||
$currentCount = (int) get_transient($rlKey);
|
if ($count >= $limit) {
|
||||||
|
return new \WP_Error('rate_limit', 'Rate limit exceeded. Retry in 60 seconds.', ['status' => 429]);
|
||||||
if ($currentCount >= $limit) {
|
|
||||||
return new \WP_Error('rate_limit_exceeded', 'Rate limit exceeded. Retry in 60 seconds.', ['status' => 429]);
|
|
||||||
}
|
}
|
||||||
set_transient($rlKey, $currentCount + 1, 60);
|
set_transient($rlKey, $count + 1, 60);
|
||||||
|
|
||||||
// Scope check — admin keys bypass all scope requirements
|
// Scope check
|
||||||
$scopes = array_map('trim', explode(',', $row['scopes']));
|
$scopes = array_map('trim', explode(',', $row['scopes']));
|
||||||
if (!in_array($requiredScope, $scopes, true) && !in_array(self::SCOPE_ADMIN, $scopes, true)) {
|
if (!in_array($requiredScope, $scopes, true) && !in_array(self::SCOPE_ADMIN, $scopes, true)) {
|
||||||
return new \WP_Error(
|
return new \WP_Error('insufficient_scope', "Required scope: {$requiredScope}", ['status' => 403]);
|
||||||
'insufficient_scope',
|
|
||||||
"Insufficient scope. Required: {$requiredScope}",
|
|
||||||
['status' => 403]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record last_used on shutdown
|
// Record last_used async
|
||||||
add_action('shutdown', function () use ($wpdb, $table, $apiKey): void {
|
add_action('shutdown', function () use ($row): void {
|
||||||
$wpdb->update($table, ['last_used' => current_time('mysql')], ['api_key' => $apiKey]);
|
global $wpdb;
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'asktam_mcp_keys',
|
||||||
|
['last_used' => current_time('mysql')],
|
||||||
|
['id' => $row['id']]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static function extractToken(\WP_REST_Request $request): string {
|
||||||
* Extract API key from request.
|
$auth = $request->get_header('Authorization');
|
||||||
* Checks: Authorization: Bearer <key> then X-MCP-API-Key header.
|
if ($auth && str_starts_with($auth, 'Bearer ')) {
|
||||||
*/
|
return trim(substr($auth, 7));
|
||||||
private static function extractKey(\WP_REST_Request $request): string {
|
|
||||||
// 1. Bearer token — Claude.ai connectors use this
|
|
||||||
$authHeader = $request->get_header('Authorization');
|
|
||||||
if ($authHeader && str_starts_with($authHeader, 'Bearer ')) {
|
|
||||||
return trim(substr($authHeader, 7));
|
|
||||||
}
|
}
|
||||||
|
// X-MCP-API-Key header for OpenAI / direct use
|
||||||
// 2. X-MCP-API-Key — OpenAI GPT Actions + direct use
|
|
||||||
$xKey = $request->get_header('X-MCP-API-Key');
|
$xKey = $request->get_header('X-MCP-API-Key');
|
||||||
if ($xKey) {
|
if ($xKey) return trim($xKey);
|
||||||
return trim($xKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a cryptographically secure API key.
|
|
||||||
*/
|
|
||||||
public static function generateKey(): string {
|
public static function generateKey(): string {
|
||||||
return 'mcp_' . bin2hex(random_bytes(24));
|
return 'mcp_' . bin2hex(random_bytes(24));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user