WordPress Application Passwords for Secure REST API Authentication

WordPress Application Passwords (introduced in WordPress 5.6) provide a mechanism for external applications, scripts, and services to authenticate against the WordPress REST API and XML-RPC without exposing the user’s main login password. Each application password is a 24-character credential generated per-user in the WordPress admin under Users → Profile → Application Passwords, or programmatically via WP_Application_Passwords::create_new_application_password() — it is displayed to the user once in plaintext and then stored as a bcrypt hash in the _application_passwords user meta key, so a database compromise does not expose usable credentials. Authentication uses HTTP Basic Auth with the WordPress username as the user and the application password as the password (spaces in the 24-character credential are optional). Every application password entry records its name, UUID, creation timestamp, last-used timestamp, and last-used IP address, all visible in the user’s profile — enabling individual revocation of credentials for compromised integrations without affecting other connected apps or requiring a main password change. Application passwords are disabled by default on HTTP sites — wp_is_application_passwords_available() returns false unless the site uses HTTPS, preventing plaintext credential transmission; disable the feature entirely with add_filter( 'wp_is_application_passwords_available', '__return_false' ) for sites that do not expose the REST API publicly. Alternative authentication methods: nonce-based cookie auth (wp_create_nonce( 'wp_rest' ) + X-WP-Nonce header) is session-scoped and appropriate only for JavaScript running inside wp-admin; JWT plugins issue expiring tokens suitable for mobile apps; OAuth 2.0 provides the full authorization-code flow for third-party app authorization. Application passwords sit between nonces (single-session, browser-only) and OAuth (full authorization flow) — they are ideal for server-to-server integrations and scripts that need stable, long-lived credentials tied to a specific WordPress user. The TOTP 2FA post covered securing the interactive login; application passwords complement that by providing API credentials that are separate from and unaffected by the 2FA-protected interactive session.

Problem: A Node.js inventory sync script updates WooCommerce product stock via the REST API every 5 minutes using the shop manager’s main login password in a .env file. When the shop manager changes their password, the sync breaks silently — and the credential is shared across three servers with no rotation process.

Solution: Create a dedicated inventory-sync-bot WordPress user with the shop_manager role, generate an application password for REST API use only, and add a filter that restricts that credential to the WooCommerce products endpoints — so a leaked credential cannot be used for any other operation.

// ── Create dedicated user and provision an application password ────────────
function myplugin_provision_sync_credentials(): void {
    $username = 'inventory-sync-bot';

    $user_id = username_exists( $username );
    if ( ! $user_id ) {
        $user_id = wp_insert_user( [
            'user_login' => $username,
            'user_pass'  => wp_generate_password( 32, true, true ),
            'user_email' => 'sync-bot@' . wp_parse_url( home_url(), PHP_URL_HOST ),
            'role'       => 'shop_manager',
        ] );
        if ( is_wp_error( $user_id ) ) return;
    }

    // $new_password is the plaintext credential — available only at creation time
    [ $new_password, $new_item ] = WP_Application_Passwords::create_new_application_password(
        $user_id,
        [ 'name' => 'ERP Inventory Sync v1' ]
    );

    // Store $new_password in a secrets vault, not in error_log on production
    error_log( 'App password UUID: ' . $new_item['uuid'] );
}

// ── Restrict the credential to WooCommerce products endpoints only ────────
add_filter( 'rest_authentication_errors', 'myplugin_restrict_sync_user_routes' );

function myplugin_restrict_sync_user_routes( $result ) {
    if ( ! is_user_logged_in() ) return $result;

    $user = wp_get_current_user();
    if ( 'inventory-sync-bot' !== $user->user_login ) return $result;

    $request_uri    = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) );
    $allowed_prefix = '/' . rest_get_url_prefix() . '/wc/v3/products';

    if ( false === strpos( $request_uri, $allowed_prefix ) ) {
        return new WP_Error(
            'rest_forbidden_route',
            'This credential is restricted to WooCommerce product endpoints.',
            [ 'status' => 403 ]
        );
    }
    return $result;
}

// Node.js: call the REST API using application password (HTTP Basic Auth)
import fetch from 'node-fetch';

const WP_URL    = process.env.WP_URL;
const WP_USER   = process.env.WP_USER;   // inventory-sync-bot
const WP_APP_PW = process.env.WP_APP_PW; // abcd efgh ijkl mnop qrst uvwx

const credentials = Buffer.from(`${WP_USER}:${WP_APP_PW}`).toString('base64');

async function updateProductStock(productId, stockQuantity) {
    const response = await fetch(`${WP_URL}/wp-json/wc/v3/products/${productId}`, {
        method: 'PUT',
        headers: {
            'Authorization': `Basic ${credentials}`,
            'Content-Type':  'application/json',
        },
        body: JSON.stringify({ stock_quantity: stockQuantity, manage_stock: true }),
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(`WC API ${response.status}: ${error.message}`);
    }
    return response.json();
}

# WP-CLI: manage application passwords

# Create a new application password (prints plaintext once)
wp user application-password create inventory-sync-bot "ERP Sync v2" --porcelain

# List all application passwords for a user
wp user application-password list inventory-sync-bot     --fields=uuid,name,last_used,last_ip --format=table

# Revoke a specific credential by UUID
wp user application-password delete inventory-sync-bot "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# Emergency: revoke ALL application passwords for a user
wp user application-password list inventory-sync-bot --format=ids |     xargs -I{} wp user application-password delete inventory-sync-bot {}

NOTE: Application passwords authenticate as the WordPress user and respect that user’s role capabilities exactly as if they were logged in to wp-admin. A shop_manager application password can do everything a shop manager can do — including reading all orders and creating coupons. The per-endpoint restriction filter adds an extra access-control layer but is not a substitute for assigning the correct minimal role to the dedicated user. Also note: application passwords cannot be used to log in to the wp-admin dashboard via the login form — they only work for REST API and XML-RPC authentication. Creating a dedicated service account whose only authentication mechanism is an application password effectively prevents interactive wp-admin login for that account, which is a desirable security property for automated service accounts.