['GET', 'POST'],
'callback' => [$this, 'handleAuthorize'],
'permission_callback' => '__return_true',
]);
// Token endpoint — Claude.ai POSTs here to exchange code for token
register_rest_route('asktam-mcp/v1', '/oauth/token', [
'methods' => 'POST',
'callback' => [$this, 'handleToken'],
'permission_callback' => '__return_true',
]);
// RFC 8414 discovery — Claude.ai fetches this to find all endpoints
register_rest_route('asktam-mcp/v1', '/oauth/metadata', [
'methods' => 'GET',
'callback' => [$this, 'handleMetadata'],
'permission_callback' => '__return_true',
]);
// Well-known discovery at site root (Claude.ai checks this)
add_action('rest_api_init', function () {
register_rest_route('', '/.well-known/oauth-authorization-server', [
'methods' => 'GET',
'callback' => [$this, 'handleMetadata'],
'permission_callback' => '__return_true',
]);
}, 20);
}
// -------------------------------------------------------------------------
// Authorization endpoint — shows approval UI, issues code on POST
// -------------------------------------------------------------------------
public function handleAuthorize(\WP_REST_Request $request): void {
$clientId = $request->get_param('client_id') ?? '';
$redirectUri = $request->get_param('redirect_uri') ?? '';
$state = $request->get_param('state') ?? '';
$approved = $request->get_param('_mcp_approve') ?? '';
// Validate client_id
global $wpdb;
$table = $wpdb->prefix . 'asktam_mcp_keys';
$row = $wpdb->get_row(
$wpdb->prepare("SELECT * FROM {$table} WHERE api_key = %s AND is_active = 1", $clientId),
ARRAY_A
);
if (!$row) {
wp_die('Invalid client_id. Check your API key in WP Admin → AskTam MCP.', 'MCP Auth Error', ['response' => 401]);
}
// User approved — issue code and redirect
if ($approved === '1' && $_SERVER['REQUEST_METHOD'] === 'POST') {
// Generate a short-lived auth code (stored as transient, 5 min TTL)
$code = 'mcp_code_' . bin2hex(random_bytes(16));
set_transient('asktam_mcp_code_' . $code, [
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'scopes' => $row['scopes'],
], 300);
$redirectBack = add_query_arg(['code' => $code, 'state' => $state], $redirectUri);
wp_redirect($redirectBack);
exit;
}
// Show approval page
$siteName = get_bloginfo('name');
$keyName = esc_html($row['name']);
$scopes = esc_html($row['scopes']);
$actionUrl = rest_url('asktam-mcp/v1/oauth/authorize');
?>
Authorize —
Authorize Access
An external AI client is requesting access to your WordPress site using the following API key:
get_param('grant_type') ?? '';
if ($grantType === 'authorization_code') {
return $this->handleAuthCodeExchange($request);
}
if ($grantType === 'client_credentials') {
return $this->handleClientCredentials($request);
}
return $this->oauthError('unsupported_grant_type', 'Supported: authorization_code, client_credentials', 400);
}
private function handleAuthCodeExchange(\WP_REST_Request $request): \WP_REST_Response {
$code = $request->get_param('code') ?? '';
$redirectUri = $request->get_param('redirect_uri') ?? '';
[$clientId, $clientSecret] = $this->extractCredentials($request);
if (empty($code)) {
return $this->oauthError('invalid_request', 'Missing code', 400);
}
// Retrieve and validate the auth code transient
$codeData = get_transient('asktam_mcp_code_' . $code);
if (!$codeData) {
return $this->oauthError('invalid_grant', 'Authorization code expired or invalid', 400);
}
delete_transient('asktam_mcp_code_' . $code);
// Validate client_id matches
if (!empty($clientId) && $clientId !== $codeData['client_id']) {
return $this->oauthError('invalid_client', 'client_id mismatch', 401);
}
// The access token IS the api_key — stateless, Auth::validate handles it
return new \WP_REST_Response([
'access_token' => $codeData['client_id'],
'token_type' => 'bearer',
'expires_in' => 3600 * 24 * 30, // 30 days
'scope' => $codeData['scopes'],
], 200);
}
private function handleClientCredentials(\WP_REST_Request $request): \WP_REST_Response {
[$clientId, $clientSecret] = $this->extractCredentials($request);
if (empty($clientId) || empty($clientSecret)) {
return $this->oauthError('invalid_client', 'Missing client_id or client_secret', 401);
}
global $wpdb;
$table = $wpdb->prefix . 'asktam_mcp_keys';
$row = $wpdb->get_row(
$wpdb->prepare("SELECT * FROM {$table} WHERE api_key = %s AND is_active = 1", $clientId),
ARRAY_A
);
if (!$row || !hash_equals($clientId, $clientSecret)) {
return $this->oauthError('invalid_client', 'Invalid credentials', 401);
}
return new \WP_REST_Response([
'access_token' => $clientId,
'token_type' => 'bearer',
'expires_in' => 3600,
'scope' => $row['scopes'],
], 200);
}
// -------------------------------------------------------------------------
// RFC 8414 metadata — Claude.ai discovers all endpoints from here
// -------------------------------------------------------------------------
public function handleMetadata(): \WP_REST_Response {
$base = rest_url('asktam-mcp/v1');
return new \WP_REST_Response([
'issuer' => get_site_url(),
'authorization_endpoint' => $base . '/oauth/authorize',
'token_endpoint' => $base . '/oauth/token',
'token_endpoint_auth_methods_supported' => ['client_secret_post', 'client_secret_basic', 'none'],
'grant_types_supported' => ['authorization_code', 'client_credentials'],
'response_types_supported' => ['code'],
'code_challenge_methods_supported' => ['S256', 'plain'],
'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'],
]);
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
private function extractCredentials(\WP_REST_Request $request): array {
$clientId = $request->get_param('client_id');
$clientSecret = $request->get_param('client_secret');
if (!empty($clientId) && !empty($clientSecret)) {
return [$clientId, $clientSecret];
}
$authHeader = $request->get_header('Authorization');
if ($authHeader && str_starts_with($authHeader, 'Basic ')) {
$decoded = base64_decode(substr($authHeader, 6));
if ($decoded && str_contains($decoded, ':')) {
[$clientId, $clientSecret] = explode(':', $decoded, 2);
return [trim($clientId), trim($clientSecret)];
}
}
return [$clientId ?? '', ''];
}
private function oauthError(string $error, string $description, int $status): \WP_REST_Response {
return new \WP_REST_Response([
'error' => $error,
'error_description' => $description,
], $status);
}
}