JOIN THE NEWSLETTER
ADVERTISEMENT
reserved 728 x 90
AI

The Authorization header your server threw away

A 401 with a credential you trust is usually the server, not the password. How to prove the header never arrived, and the one line that fixes it.
Title card reading The header your server threw away, on why a valid WordPress application password still returns 401

The credentials were right. I had just pasted them. The site still said 401.

If your WordPress REST API returns 401 with an application password you know is correct, the most likely cause is not the password. It is that your web server never handed the Authorization header to PHP. An Authorization header stripped at the server layer is invisible from the outside. The request looks perfect on your machine and arrives at WordPress with nothing to authenticate. You can confirm this in about a minute, and the fix is usually two lines in a config file.

KEY FACTS

The CGI specification does not pass Authorization through to the script. It was left out deliberately, so the web server could own authentication.

Anything running PHP as CGI, FastCGI or PHP-FPM inherits that behaviour. Most shared hosting does.

WordPress reads the credential from PHP_AUTH_USER and PHP_AUTH_PW. No header means no values, and no values means an anonymous request.

An anonymous request to a protected route returns 401, not 403. That is why the error reads like a bad password.

The Apache fix is one SetEnvIf line, and it has to sit above the WordPress block or it gets erased.

What this actually looks like

You send a normal authenticated request:

curl -s -u 'user:abcd EFGH ijkl MNOP qrst UVWX' \
  'https://example.com/wp-json/wp/v2/users/me'

And you get back something like this:

{"code":"rest_not_logged_in",
 "message":"You are not currently logged in.",
 "data":{"status":401}}

The tell is the wording. WordPress is not saying the password is wrong. It is saying nobody is logged in. Those are different failures. A wrong application password gives you incorrect_password. Being treated as a stranger gives you rest_not_logged_in or rest_cannot_create.

I lost an hour to this by reading 401 as “credential problem” and regenerating the password four times. The second one would have worked fine. Nothing was ever going to work, because the credential was being thrown away before WordPress could look at it.

Why the header disappears before PHP sees it

This is old behaviour, and it is on purpose.

When a web server runs a script through CGI, it copies request headers into environment variables. The CGI specification leaves Authorization off that list. The reasoning was that authentication belonged to the server, not the script, so the script had no business seeing the raw credential. Apache follows this. So does anything descended from it, which in practice means FastCGI and PHP-FPM too.

Apache running PHP as a module does not have this problem, because there is no CGI boundary to cross. That is why the same code works on your laptop and fails on your host. Local stacks often use the module. Shared hosting almost never does.

The WordPress REST API handbook covers this under authentication and gives the standard server snippets. It is worth reading the REST API frequently asked questions page once, because the same section explains a few adjacent failures.

Prove it before you change anything

Editing server config on a hunch is how you end up with two problems. I would rather see the header arrive or not arrive.

The smallest honest test is a route that reports what PHP received. Drop this in a snippet plugin or your theme functions file:

add_action('rest_api_init', function () {
  register_rest_route('sitehelper/v1', '/ping', array(
    'methods'  => 'GET',
    'callback' => function () {
      return array(
        'auth_header_present' => isset($_SERVER['HTTP_AUTHORIZATION']),
        'redirect_header'     => isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']),
        'php_auth_user'       => isset($_SERVER['PHP_AUTH_USER']),
        'user_id'             => get_current_user_id(),
      );
    },
    'permission_callback' => '__return_true',
  ));
});

Call it with the same credentials that are failing:

curl -s -u 'user:abcd EFGH ijkl MNOP qrst UVWX' \
  'https://example.com/wp-json/sitehelper/v1/ping'

Now the answer is not a guess.

All false and user_id zero means the header never arrived. That is this problem. Fix the server.

Header present but php_auth_user false means the header arrived and something is eating it after the fact. Usually a security plugin or a WAF rule.

Everything true and user_id still zero means your credential is genuinely wrong, or the user cannot do what you asked. That is a different post. If the Application Passwords panel is not even showing up on the profile screen, I wrote up the three reasons WordPress hides it separately.

Leave this route in place afterwards. It costs nothing and it answers the question again next time.

The Apache fix, and where the line has to go

Apache can be told to copy the header into an environment variable that PHP will read. Two forms work. This is the one the handbook gives:

<IfModule mod_setenvif>
  SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
</IfModule>

And this is the rewrite form, which I use because rewrite is enabled everywhere WordPress runs and mod_setenvif occasionally is not:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{HTTP:Authorization} .
  RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</IfModule>

Either one is fine. Do not add both.

Here is the part that cost me a second hour. The line has to go above the # BEGIN WordPress marker in your .htaccess file.

WordPress owns everything between # BEGIN WordPress and # END WordPress. It rewrites that region whenever permalinks get saved, and plenty of hosts and plugins regenerate it on their own schedule. Anything you put inside that block is temporary. It will work, you will move on, and it will vanish a week later when something touches permalink settings. Then you get to debug the same 401 twice.

So the file reads top to bottom like this:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{HTTP:Authorization} .
  RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</IfModule>

# BEGIN WordPress
# ... do not touch anything in here ...
# END WordPress

Save it, then call the ping route again. On my site auth_header_present flipped to true immediately and the real REST calls started returning 200.

One thing to watch. Some stacks deliver it as REDIRECT_HTTP_AUTHORIZATION instead, because the rewrite counts as an internal redirect and Apache prefixes the variable. That is why the ping route above checks both. WordPress core handles the redirect form, so if you see that one set, you are fine.

What the Nginx advice usually gets wrong

Search this problem and you will be told to add this to your Nginx FastCGI block:

fastcgi_pass_header Authorization;

I want to flag something here, because I went and read the manual rather than copying the snippet.

The Nginx documentation for the FastCGI module describes fastcgi_pass_header as permitting header fields to pass from the FastCGI server to the client. That is the response direction. The header we care about is travelling the other way, from the client into PHP.

The directive that puts a request header into PHP is the parameter one:

fastcgi_param HTTP_AUTHORIZATION $http_authorization;

I run Apache, so I have not tested this on a live Nginx host and I am not going to claim I have. What I can say is that the widely copied line does not match what the directive is documented to do, and the parameter form does. If you are on Nginx, use the ping route to check which one actually moves the needle. That takes thirty seconds and beats trusting either me or a forum post from 2019.

When the server config is not yours to change

Some hosting will not let you touch .htaccess in a way that survives, or strips the header further upstream at a proxy you have no access to. I hit a version of this before I got the rewrite line to stick.

The workaround is to send the credential under a header name nothing filters, then put it back where WordPress expects it. Send your request with a custom header:

curl -s -H "X-Relay-Authorization: Basic dXNlcjpwYXNz" \
  'https://example.com/wp-json/wp/v2/users/me'

And copy it across early, before authentication runs:

if (!empty($_SERVER['HTTP_X_RELAY_AUTHORIZATION'])
    && empty($_SERVER['HTTP_AUTHORIZATION'])) {
  $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['HTTP_X_RELAY_AUTHORIZATION'];
}

Be clear eyed about what this is. It is a workaround, not a fix. You are moving a Basic Auth credential into a header that no standard protects. So it only belongs on HTTPS. It also widens the surface, because any code path that logs request headers now logs your password in a new place. I keep it as a fallback and prefer the .htaccess line whenever the host allows it.

It is worth saying that this class of problem is why I automate against a real diagnostic endpoint instead of hoping. The same discipline applies to the whole setup I described in running WordPress over the REST API, where every write gets verified against what actually rendered rather than what got sent.

The order I check things in

Now that I have been through it, this is the sequence that would have saved me two hours.

First, read the error code, not the status. rest_not_logged_in is a missing credential. incorrect_password is a wrong one. Stop treating 401 as one thing.

Second, call a ping route and look at whether the header arrived. Everything downstream depends on that one boolean.

Third, if the header is missing, add the rewrite line above the WordPress block in .htaccess and test again.

Fourth, if the header arrives but PHP still shows no user, start disabling security plugins one at a time. A firewall rule that drops Authorization looks identical to a server that never passed it, and the ping route is what tells them apart.

Fifth, if all of that is clean, the credential really is the problem. Regenerate it then, and not before.

Google’s own tooling hits related walls for its own reasons, which I went into in the Site Kit scopes writeup. Different cause, same shape of afternoon.

FAQ

Why does a missing Authorization header return 401 instead of 403?

Because 401 means unauthenticated and 403 means authenticated but not permitted. With no header, WordPress sees an anonymous visitor, so the honest answer is 401. The status is correct. It is just easy to misread as a rejected password.

How do I know whether my host runs PHP as a module or through FastCGI?

Check the PHP server API value. A quick route returning php_sapi_name() will say apache2handler for the module, or fpm-fcgi or cgi-fcgi for the others. If it is not the module, expect to add the rewrite line.

Will adding the rewrite line break anything else?

I have not seen it cause a problem, and it is the documented fix. It sets one environment variable and changes no routing. The realistic risk is a typo in .htaccess taking the site down with a 500, so keep a copy of the file before you edit it.

My fix worked and then stopped working a week later. Why?

Almost certainly because the line was inside the block between the WordPress begin and end markers, and something rewrote that region. Saving permalink settings does it. So do some host optimisation tools. Move the line above the marker.

Does this affect anything besides application passwords?

Yes. Anything that authenticates with an Authorization header hits the same wall, including JWT plugins, OAuth bearer tokens and most third party integrations. The symptom is always the same, which is a request that looks authenticated from outside and anonymous from inside.

Can I test this without editing any files at all?

Partly. If you have a plugin that lets you register a snippet, the ping route needs no file editing. The .htaccess change does need file access, either over SFTP or through a hosting file manager. There is no way to set an Apache environment variable from inside PHP after the fact, because PHP has already started by then.

The short version

A 401 with a credential you trust is usually not about the credential. Apache, FastCGI and PHP-FPM drop the Authorization header by design, WordPress sees an anonymous request, and it tells you so in the only status code that fits.

Prove it with a route that reports what PHP received. Add the rewrite line above the WordPress block. Test again. Then go back to whatever you were actually trying to build.

Once the API worked, the next fight was with Google itself. That one is in Search Console never read my sitemap.

MP
Written by Marcos Ponce
I build things and write down what actually happened: AI automation, micro-cap deep dives, 3D printed PC cases, and the occasional song.
ADVERTISEMENT
reserved 336 x 280

One useful build a week.

The next automation, deep dive, or 3D print, written up the way I wish someone had written it for me.

SUBSCRIBE