get_header( 'authorization' ); if ( $auth_header && preg_match( '/^Bearer\s+(.+)$/i', $auth_header, $matches ) ) { $result = self::authenticate_api_key( trim( $matches[1] ) ); if ( is_wp_error( $result ) ) { return $result; } return 'key:' . self::$authenticated_key_id; } // Application Password (Basic auth) resolved by core. A plain cookie session // is intentionally NOT accepted here: without a nonce that would be a CSRF // vector, and MCP clients always authenticate explicitly. $user = wp_get_current_user(); if ( $user && $user->exists() && user_can( $user, 'read' ) ) { return 'user:' . $user->ID; } return new WP_Error( 'agentdesk_unauthorized', __( 'Authentication required. Use Application Password (Basic) or Bearer API key.', 'agentdesk-site-operations' ), array( 'status' => 401 ) ); } /** * ID of the API key matched on the current request (for per-key rate limiting). * * @var string */ private static $authenticated_key_id = ''; /** * Authenticate Bearer API key. * * @param string $token Raw token. * @return true|WP_Error */ public static function authenticate_api_key( $token ) { if ( empty( $token ) ) { return new WP_Error( 'agentdesk_invalid_key', __( 'Empty API key.', 'agentdesk-site-operations' ), array( 'status' => 401 ) ); } // Structural pre-check before any bcrypt work: keys are always "agentdesk_" + 40 // url-safe chars (see create_api_key). Rejecting malformed tokens here means a // flood of garbage Bearer tokens can't force expensive password_verify() calls. if ( ! preg_match( '/^agentdesk_[A-Za-z0-9]{40}$/', $token ) ) { return new WP_Error( 'agentdesk_invalid_key', __( 'Invalid API key.', 'agentdesk-site-operations' ), array( 'status' => 401 ) ); } $settings = Agentdesk_Security::get_settings(); $keys = isset( $settings['api_keys'] ) && is_array( $settings['api_keys'] ) ? $settings['api_keys'] : array(); foreach ( $keys as $entry ) { if ( empty( $entry['hash'] ) || empty( $entry['user_id'] ) ) { continue; } if ( ! empty( $entry['revoked'] ) ) { continue; } if ( password_verify( $token, $entry['hash'] ) ) { wp_set_current_user( (int) $entry['user_id'] ); self::$authenticated_key_id = isset( $entry['id'] ) ? (string) $entry['id'] : (string) $entry['user_id']; return true; } } return new WP_Error( 'agentdesk_invalid_key', __( 'Invalid API key.', 'agentdesk-site-operations' ), array( 'status' => 401 ) ); } /** * Generate a new API key for a user. * * @param int $user_id User ID. * @param string $label Key label. * @return array{token: string, id: string}|WP_Error */ public static function create_api_key( $user_id, $label = '' ) { if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'agentdesk_forbidden', __( 'Only administrators can create API keys.', 'agentdesk-site-operations' ) ); } $token = 'agentdesk_' . wp_generate_password( 40, false, false ); $id = 'key_' . wp_generate_password( 12, false, false ); $hash = password_hash( $token, PASSWORD_DEFAULT ); $settings = Agentdesk_Security::get_settings(); $settings['api_keys'][] = array( 'id' => $id, 'label' => sanitize_text_field( $label ), 'hash' => $hash, 'user_id' => absint( $user_id ), 'created_at' => current_time( 'mysql', true ), 'revoked' => false, ); Agentdesk_Security::update_settings( $settings ); return array( 'token' => $token, 'id' => $id, ); } /** * Revoke an API key by ID. * * @param string $key_id Key ID. * @return true|WP_Error */ public static function revoke_api_key( $key_id ) { if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'agentdesk_forbidden', __( 'Only administrators can revoke API keys.', 'agentdesk-site-operations' ) ); } $settings = Agentdesk_Security::get_settings(); $updated = false; foreach ( $settings['api_keys'] as &$entry ) { if ( isset( $entry['id'] ) && $entry['id'] === $key_id ) { $entry['revoked'] = true; $updated = true; } } unset( $entry ); if ( ! $updated ) { return new WP_Error( 'agentdesk_not_found', __( 'API key not found.', 'agentdesk-site-operations' ) ); } Agentdesk_Security::update_settings( $settings ); return true; } }