July 22, 2026

The Role That Wasn't the Problem

A colleague couldn't log into the WooCommerce app. I spent an afternoon fixing her user role. The role was never it — the server was eating the auth header.

  • WooCommerce
  • WordPress
  • REST API
  • Debugging
Illustration of a valid user badge beside an authenticated request blocked by a wall

A colleague couldn’t log into the WooCommerce mobile app. Every sign pointed at her user role. I changed the role three times. The role was never the problem — the server was quietly eating the Authorization header.

It started, as these things do, with a screenshot in a chat.

She was trying to sign into the WooCommerce iOS app with her store credentials. Address correct, username correct, password correct. And then a dark little toast at the bottom of the screen:

We couldn’t verify your role on this store. Please try again. If the problem persists, contact support.

“Couldn’t verify your role.” The app is practically pointing at the answer. Her account had a custom role — admin_manager — and the WooCommerce app only understands a handful of standard ones: administrator, shop_manager, editor. A custom role it’s never heard of? Of course it can’t verify it. Case closed.

Except it wasn’t.

The theory that fit too well

This is the trap, and I walked straight into it: the error message agreed with my first theory, so I stopped looking.

I opened her user in wp-admin and switched the role from admin_manager to Administrator. Clean, standard, unambiguous. Told her to try again.

Same error.

Okay — stale session. The app caches the role from the first login; changing it server-side doesn’t retroactively update a live session. So I hit “Log out everywhere” on her profile to nuke every session, and had her sign in fresh.

Same error.

Now I’m annoyed, because the theory was good. A custom role, an app that whitelists roles, an error message that literally says “role.” Everything lined up. When your explanation and the error message are this friendly with each other, it feels like confirmation. It’s not. It’s just two things pointing the same wrong direction.

Lesson #1: An error message describes the symptom the app decided to show you, not the layer where things actually broke. “Couldn’t verify your role” and “the role is wrong” are not the same sentence.

The second error that broke the story

To take her account out of the equation, I tried logging in with my own WordPress admin account — a real, boring, unambiguous administrator. If roles were the issue, mine would sail through.

It didn’t. But it failed differently:

Error fetching user information.

That stopped me. Two accounts, two different errors. Her admin_manager said “couldn’t verify your role.” My clean administrator said “error fetching user information.” If the problem were roles, my account would either work or fail the same way hers did. A different failure on a valid admin meant the role story was dead — I’d just been too invested to notice.

“Error fetching user information” is what the app says when it authenticates, then goes to fetch the current user from the REST API — /wp-json/wp/v2/users/me — and doesn’t get back what it expects.

So the question changed from “what’s wrong with the role” to “what happens when something asks this site who I am.”

Following the request instead of the message

First, the naive check. Open the endpoint in a logged-in browser:

https://shop.example.com/wp-json/wp/v2/users/me
{"code":"rest_not_logged_in","message":"You are not currently logged in.","data":{"status":401}}

A 401. For half a second that looks like a smoking gun. It isn’t — and this is the second place the investigation tried to mislead me. Browser cookie auth over REST needs a nonce, and a plain address-bar visit doesn’t carry one. So WordPress correctly says “not logged in.” This 401 is normal. It only tells you the request reached WordPress. File it away — that detail matters in about thirty seconds.

The app doesn’t use cookies anyway. It uses an Application Password — HTTP Basic Auth in an Authorization header. So I reproduced that, with curl, using a real application password:

curl -u "admin_dev:xxxx xxxx xxxx xxxx xxxx xxxx" \
  https://shop.example.com/wp-json/wp/v2/users/me

And out came this:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>403 Forbidden</title>
  <link rel="stylesheet" href="/error_docs/styles.css">
</head>
<body>
  <div class="page"><div class="main">
    <h1>Server Error</h1>
    <div class="error-code">403</div>
    <h2>Forbidden</h2>
    <p class="lead">You do not have permission to access this document.</p>

Look at what that is. It’s not a WordPress response. There’s no {"code": ...} JSON, no WordPress theming — it’s a bare server error page, styled from /error_docs/styles.css. That page comes from the web server / firewall, not from PHP. WordPress never ran. The request was killed at the door.

Now put the two requests side by side:

  • Browser, no Authorization header → reached WordPress, got a polite JSON 401.
  • curl, with an Authorization header → killed by the server with a raw 403 before WordPress woke up.

The only meaningful difference between those two requests is the Authorization header. Add Basic Auth and the request stops existing.

Lesson #2: Learn to tell who answered you. A JSON error with a code field is WordPress talking. An HTML page with a server error template and its own stylesheet is the web server or WAF talking — and it means your request never reached the application at all.

The actual culprit

A WAF / mod_security rule (extremely common on managed WordPress hosting) was blocking requests to wp-json that carried an Authorization header. Basic Auth on the REST API tripped a “someone’s trying to brute-force auth” heuristic, and the firewall returned its own 403 before PHP ever saw the request.

Which explains everything, cleanly:

  • The mobile app authenticates with Basic Auth → firewall blocks it → the app can’t fetch the user → “Error fetching user information.”
  • It had nothing to do with the role. It never did.
  • And the reason the “role” error looked so convincing on the first account is that it was a different app build showing a different generic message for the same underlying failure to read the user.

The role was a coincidence dressed up as a cause.

Lesson #3: When two people hit “the same bug” with two different error messages, don’t average them into one theory. The discrepancy is the clue. Identical root causes tend to produce identical failures.

The fix

This one doesn’t live in the theme or in WordPress at all — it’s the hosting layer:

  • Whitelist Basic Auth on the REST API. Ask the host (or edit the WAF / mod_security config) to allow an Authorization header on /wp-json/wp/v2/users and /wp-json/wc/*. That’s the durable fix.
  • Confirm the header even survives to PHP. Some stacks strip Authorization before it reaches WordPress; a one-line .htaccess rewrite puts it back:
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
  • Rule out a User-Agent block while you’re there. Firewalls love to 403 anything that looks like curl. Re-run the request with a browser UA (curl -A "Mozilla/5.0" ...); if that suddenly works, the block was on the User-Agent, not the header — a different rule, same lesson.

What I actually took away

I lost an afternoon not to a hard problem but to an agreeable one. The very first error message handed me a theory, the user’s account happened to have a matching quirk, and the two reinforced each other until I mistook coincidence for confirmation. I changed a role three times. I logged sessions out. I did everything the story wanted — and the story was fiction.

The moment I actually made progress was the moment a second, different error showed up on an account that had no business failing. That contradiction was worth more than any log line, because it broke the comfortable narrative and forced me to follow the request itself instead of the words on the screen.

This is the same shape of bug as the day fail2ban banned our whole office: the application was innocent, the infrastructure in front of it was the one making decisions, and every minute spent inside the app was a minute spent in the wrong building.

Watch who answers you. WordPress speaks JSON. The firewall speaks HTML. And when a request dies before the application even sees it, no amount of fixing the application will ever help.