'GET', 'callback' => array( __CLASS__, 'issue_token' ), 'permission_callback' => array( __CLASS__, 'logged_in_permission' ), 'args' => array( 'session_key' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => array( Agentmatica_WooCommerce_Cart::class, 'sanitize_session_key' ), ), ), ) ); } public static function logged_in_permission(): bool { return is_user_logged_in(); } /** * @param WP_REST_Request $request * @return WP_REST_Response|WP_Error */ public static function issue_token( WP_REST_Request $request ) { if ( ! Agentmatica_WooCommerce_Orders::is_order_lookup_enabled() ) { return new WP_Error( 'agentmatica_order_lookup_disabled', __( 'Order lookup is disabled on this store.', 'agentmatica-mcp-connector' ), array( 'status' => 403 ) ); } $session_key = Agentmatica_WooCommerce_Cart::sanitize_session_key( (string) $request->get_param( 'session_key' ) ); if ( $session_key === '' ) { return new WP_Error( 'agentmatica_invalid_session', __( 'session_key is required.', 'agentmatica-mcp-connector' ), array( 'status' => 400 ) ); } $user_id = get_current_user_id(); $user = get_user_by( 'id', $user_id ); if ( ! $user instanceof WP_User ) { return new WP_Error( 'agentmatica_not_logged_in', __( 'You must be logged in.', 'agentmatica-mcp-connector' ), array( 'status' => 401 ) ); } Agentmatica_WooCommerce_Orders::bind_logged_in_customer( array( 'session_key' => $session_key, 'wp_user_id' => $user_id, ) ); $exp = time() + self::TOKEN_TTL; $token = self::sign_token( array( 'wp_user_id' => $user_id, 'session_key' => $session_key, 'email' => strtolower( trim( (string) $user->user_email ) ), 'name' => trim( (string) $user->display_name ), 'exp' => $exp, ) ); return rest_ensure_response( array( 'token' => $token, 'expires_at' => gmdate( DATE_ATOM, $exp ), 'customer' => array( 'id' => $user_id, 'email' => strtolower( trim( (string) $user->user_email ) ), 'name' => trim( (string) $user->display_name ), ), ) ); } /** * @param array $claims */ public static function sign_token( array $claims ): string { $secret = self::get_secret(); $json = wp_json_encode( $claims ); $payload = self::base64url_encode( false === $json ? '{}' : $json ); $signature = hash_hmac( 'sha256', $payload, $secret, true ); return $payload . '.' . self::base64url_encode( $signature ); } /** * @return array|null */ public static function verify_token( string $token ): ?array { $parts = explode( '.', $token, 2 ); if ( count( $parts ) !== 2 ) { return null; } list( $payload, $signature ) = $parts; $secret = self::get_secret(); $expected = self::base64url_encode( hash_hmac( 'sha256', $payload, $secret, true ) ); if ( ! hash_equals( $expected, $signature ) ) { return null; } $decoded = json_decode( self::base64url_decode( $payload ), true ); if ( ! is_array( $decoded ) ) { return null; } $exp = isset( $decoded['exp'] ) ? (int) $decoded['exp'] : 0; if ( $exp <= time() ) { return null; } return $decoded; } private static function get_secret(): string { $secret = (string) get_option( self::SECRET_OPTION, '' ); if ( $secret === '' ) { $secret = wp_generate_password( 64, true, true ); update_option( self::SECRET_OPTION, $secret, false ); } return $secret; } private static function base64url_encode( string $value ): string { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- base64url for signed tokens, not obfuscation. return rtrim( strtr( base64_encode( $value ), '+/', '-_' ), '=' ); } private static function base64url_decode( string $value ): string { $remainder = strlen( $value ) % 4; if ( $remainder > 0 ) { $value .= str_repeat( '=', 4 - $remainder ); } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- base64url for signed tokens, not obfuscation. $decoded = base64_decode( strtr( $value, '-_', '+/' ), true ); return is_string( $decoded ) ? $decoded : ''; } }