Resolving WordPress REST API error 401 unauthorized during application password sync

Root Cause Diagnosis

A 401 Unauthorized on the WordPress REST API during application password authentication almost always means the Authorization header never reaches PHP, not that the credentials are wrong. Apache with mod_php/mod_fcgid, Nginx with PHP-FPM, and most shared-hosting proxy layers strip the Authorization header by default because it is not passed through by the SAPI or forwarded by the reverse proxy. WordPress falls back to PHP_AUTH_USER/PHP_AUTH_PW, which are populated only when the header survives, so if it’s stripped, wp_authenticate_application_password() never runs and any authenticated route returns rest_not_logged_in or rest_forbidden with status 401. Secondary causes are Application Passwords being disabled via filter, the REST route being blocked by a security plugin, or the site running over plain HTTP with WP_ENVIRONMENT_TYPE forcing HTTPS-only cookies/nonces.

Quick Fix

Confirm the header is missing, then restore it at the server layer.

curl -s -o /dev/null -w "%{http_code}\n" -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" https://example.com/wp-json/wp/v2/users/me

If this fails but the same request succeeds locally against wp-json with WP_DEBUG showing no auth headers, add this to wp-config.php above /* That's all, stop editing! */ as an immediate workaround:

if ( ! isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
    if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
        $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
    }
}

This only helps when Apache exposes the header under REDIRECT_HTTP_AUTHORIZATION, which is the common case with mod_php/mod_fcgid.

Step-by-Step Resolution

1. Verify the header actually arrives at PHP

Create a throwaway script at wp-content/debug-auth.php:

<?php
header('Content-Type: text/plain');
foreach ($_SERVER as $k => $v) {
    if (stripos($k, 'auth') !== false) echo "$k: $v\n";
}
echo "getallheaders(): \n";
print_r(function_exists('getallheaders') ? getallheaders() : 'N/A');

Request it with the same credentials:

curl -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" https://example.com/wp-content/debug-auth.php
  • Empty output: the header is stripped before PHP.
  • REDIRECT_HTTP_AUTHORIZATION present but not HTTP_AUTHORIZATION: Apache-specific stripping (Step 2).
  • Header present and correct: the problem is in WordPress/plugin logic, not transport (Step 5).
See also  WooCommerce order stock status not reducing on completed COD orders

Delete this file after testing.

2. Apache with mod_php or mod_fcgid

Add to .htaccess above the WordPress block, or to the vhost config:

RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]

If using mod_fcgid/mod_fastcgi directly (not mod_php), also set:

CGIPassAuth On

Requires Apache 2.4.13+. Reload:

sudo apachectl configtest && sudo systemctl reload apache2

3. Nginx with PHP-FPM

FastCGI does not forward Authorization by default. In the server or location block handling PHP:

location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_param HTTP_AUTHORIZATION $http_authorization;
    include fastcgi_params;
}

Reload:

sudo nginx -t && sudo systemctl reload nginx

4. Reverse proxy or CDN in front of origin (Cloudflare, load balancer, Nginx proxy layer)

Ensure the proxy does not drop the header when forwarding:

proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;

For Cloudflare, the Authorization header passes through by default; a 401 in this setup usually indicates a Cloudflare Access policy or WAF custom rule intercepting the request before it reaches origin. Check Security > WAF > Firewall rules/events for a block on /wp-json/*.

5. Rule out plugin- or code-level blocks

Application Passwords can be disabled by a filter, commonly added by security plugins:

add_filter( 'wp_is_application_passwords_available', '__return_false' );

Search the theme and active plugins:

grep -r "wp_is_application_passwords_available" wp-content/
grep -r "application_password" wp-content/plugins/ wp-content/themes/ --include=*.php -l

Also confirm the REST route itself is not blocked:

curl -s https://example.com/wp-json/ | head -c 300

If this returns 401 or 403 before credentials are even sent, a security plugin (Wordfence, iThemes Security, All In One WP Security) is restricting /wp-json/. Check its firewall/API-access settings for an REST API restriction.

6. Confirm HTTPS and site URL configuration

Application Passwords require HTTPS in production; WordPress blocks the feature over plain HTTP unless the site is on localhost or a .test/.local domain. Check:

wp option get siteurl
wp option get home

Both must be https://. Mismatched WP_HOME/WP_SITEURL in wp-config.php against the actual serving domain also triggers auth failures via cookie/nonce mismatches on some protected routes.

See also  Rank Math SEO Schema Warning: Missing Field "hasCourseInstance" on Custom Post Type

7. Regenerate the application password and re-test

Stale copies with stripped or added whitespace are a frequent cause of a 401 that looks like a transport issue. Regenerate under Users > Profile > Application Passwords, and pass it to curl inside quotes exactly as displayed, spaces included:

curl -u "username:abcd efgh ijkl mnop qrst uvwx" https://example.com/wp-json/wp/v2/users/me

Why This Fails for Some Users

  • Shared hosting without config access: Many shared hosts run PHP as CGI/suPHP with no ability to edit Apache/Nginx config or .htaccess rewrite behavior for headers. The wp-config.php REDIRECT_HTTP_AUTHORIZATION fallback is the only available workaround; if the host also strips that variable, application passwords cannot work until the host enables CGIPassAuth or equivalent.
  • LiteSpeed / OpenLiteSpeed: Requires Rewrite Rule context similar to Apache, but some versions need CGIPassAuth On in the vhost’s “External App” config rather than .htaccess, and this setting is only exposed in the LiteSpeed WebAdmin console, not per-site.
  • WooCommerce or multisite REST namespaces: These use permission callbacks that check capabilities per subsite; a valid application password can still return 401/403 on a network install if the user lacks the role on that specific subsite, independent of the header issue.
  • SCRIPT_URI/REQUEST_URI rewriting by security modules: ModSecurity rulesets (especially OWASP CRS on some hosts) block requests containing Authorization: Basic patterns as a false-positive SQLi/credential-stuffing signature. Check /var/log/apache2/modsec_audit.log or the hosting panel’s WAF log for a matching rule ID and add an exception for /wp-json/.
  • Reverse proxy stripping on WP Engine, Kinsta, and similar managed hosts: Some managed platforms terminate SSL and proxy through infrastructure that does not forward Authorization and gives no server-config access. These hosts require opening a support ticket to whitelist the header; no client-side fix exists.
  • WP_ENVIRONMENT_TYPE=local or staging flags: Setting this constant changes some capability and cookie behavior and can interact with Application Passwords’ localhost-exception logic in unexpected ways on staging domains that are not literally localhost.
  • Two-Factor plugins intercepting REST auth: Some 2FA plugins hook authenticate or rest_authentication_errors ahead of the application-password handler and reject the request before it evaluates the password, returning a generic 401 unrelated to header transport.
  • PHP running via CGI with cgi.fix_pathinfo and older PHP (5.x/7.0 EOL environments): Older CGI SAPIs never populate HTTP_AUTHORIZATION under any Apache directive; upgrading to PHP-FPM or a supported PHP version is the only fix.
  • Case-sensitive proxy_set_header conflicts: Defining fastcgi_param HTTP_AUTHORIZATION in multiple included Nginx snippet files causes the last-loaded value (sometimes empty) to win silently, with no error logged.
See also  Resolving mixed content HTTP warnings on WooCommerce SVG icons behind Cloudflare SSL

Leave a Comment