Headless WordPress Authentication: JWT vs Application Passwords vs OAuth

A hands-on comparison of the three real authentication options for headless WordPress, from someone who built one of them.

Monday, March 16, 2026
Enrique Chavez

The moment your frontend moves to a separate origin from WordPress, the default cookie-based flow gets awkward fast. Your React app, your Next.js site, or your mobile app doesn't automatically get the same cookie + nonce setup that WordPress expects for its own admin and theme requests.

You need a different approach. And you basically have three options: Application Passwords (built into WordPress core), JWT tokens, or OAuth 2.0.

I've maintained JWT Authentication for WP REST API since 2015 — it's now on 60,000+ WordPress sites. I also built JWT Auth Pro. So yes, I have a bias. I'll be upfront about that. But I've also spent years watching developers try all three approaches, reading their support tickets, and helping them debug authentication failures at 2am. That gives me a perspective most comparison posts don't have.

This post breaks down each method honestly — what works, what doesn't, and when you should use each one.

The Authentication Problem in Headless WordPress

Traditional WordPress authentication uses cookies. You log in at wp-login.php, WordPress sets a cookie, and requests coming from WordPress itself can send the nonce/header combination the REST API expects. Simple.

Headless WordPress complicates this model. Your frontend lives at app.example.com, your WordPress backend lives at api.example.com. Separate origins. You can make cross-origin cookies work, but now you're managing CORS, credentialed requests, cookie settings, and nonce handling instead of using the API like a clean remote client.

You need stateless authentication — something the frontend can store and send explicitly with each API request. That's where tokens come in.

Application Passwords — The Built-in Option

WordPress 5.6 shipped Application Passwords in December 2020. No plugin needed. You generate a password from your user profile, and use it with Basic Authentication to hit the REST API.

Here's what that looks like:

Authenticating with Application Passwords
# Generate base64-encoded credentials
echo -n "username:xxxx xxxx xxxx xxxx xxxx xxxx" | base64

# Use them in a request
curl -X GET https://yoursite.com/wp-json/wp/v2/posts \
  -H "Authorization: Basic dXNlcm5hbWU6eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHg="

And in JavaScript:

src/lib/wp-client.js
const WP_URL = 'https://yoursite.com/wp-json';
const credentials = btoa('username:xxxx xxxx xxxx xxxx xxxx xxxx');

async function fetchPosts() {
  const response = await fetch(`${WP_URL}/wp/v2/posts`, {
    headers: {
      'Authorization': `Basic ${credentials}`,
    },
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  return response.json();
}

That's it. No plugin, no token exchange, no refresh logic. It just works.

But here's the catch: you're sending a long-lived credential with every single request. The username and application password are only base64-encoded, so HTTPS is non-negotiable. If that credential leaks, it stays valid until you revoke that specific application password. There's no built-in expiration or refresh flow, and you're revoking the credential itself rather than a short-lived session token.

WordPress stores Application Passwords hashed and makes them individually revocable, which is good. But auth still involves looking up the user and checking that credential on each request. The bigger trade-off is credential lifecycle, not setup speed.

For a quick prototype, an internal tool, or a service-to-service integration where you control both endpoints, Application Passwords are genuinely the fastest path. I'd use them myself for a quick admin script or a CI/CD pipeline that needs to push content to WordPress. They can also be fine for trusted desktop or mobile clients if you accept the trade-offs.

For a user-facing headless app with real sessions to manage? I usually want JWT instead.

JWT — The Token-Based Approach

JWT (JSON Web Tokens) works differently. Instead of sending credentials with every request, you authenticate once, get a signed token, and send that token with subsequent requests. The token usually carries standard claims like issuer, issued-at, expiry, and at least a user identifier. The important difference is that the client stops sending a long-lived credential on every request.

The flow:

  1. Frontend sends username + password to /wp-json/jwt-auth/v1/token
  2. WordPress validates the credentials and returns a signed JWT (plus a refresh token, if you're using JWT Auth Pro)
  3. Frontend stores the token and includes it as a Bearer token in every API request
  4. When the token expires, the frontend uses the refresh token to get a new one — no re-authentication needed

Here's the authentication request:

Requesting a JWT token
curl -X POST https://yoursite.com/wp-json/jwt-auth/v1/token \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "your-password"}'

The response from JWT Auth Pro:

JWT Auth Pro token response
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "user_id": 1,
  "user_email": "[email protected]",
  "user_nicename": "admin",
  "user_display_name": "Enrique Chavez",
  "refresh_token": "dWvOqLPVDjkNeCGOBVkR1bW..."
}

Then every authenticated request uses the token:

src/lib/wp-api.js
async function fetchProtectedData(endpoint, token) {
  const response = await fetch(`https://yoursite.com/wp-json${endpoint}`, {
    headers: {
      'Authorization': `Bearer ${token}`,
    },
  });

  if (response.status === 403) {
    const error = await response.json();
    if (error.code === 'jwt_auth_invalid_token') {
      // Token expired — refresh it
      return null;
    }
  }

  return response.json();
}

The big difference from Application Passwords: the actual credentials (username and password) only travel once, during the initial authentication. After that, only the signed token moves between frontend and backend. If a token leaks, it's usually time-limited. If you need to kill access early, JWT Auth Pro lets you revoke that token without touching the user's password.

JWT Auth Pro adds refresh tokens, a token management dashboard, configurable expiration times, and analytics on who's accessing your API. The free version of the plugin handles basic token generation and validation — if that's all you need, it works.

The trade-off: JWT requires a plugin. Application Passwords don't. If "zero dependencies" matters more to you than token management and expiration, that's a legitimate reason to choose Application Passwords. I won't pretend otherwise.

The pattern I see most often: developers start with Application Passwords because they're quick. Then the project grows. They need short-lived access tokens. They need to see which tokens are active. They need refresh tokens so the mobile app doesn't force a re-login every hour. They want automated revocation rules when account state changes. That's when they land on JWT — not because someone told them to, but because they hit a wall with AppPass that token-based auth handles better.

OAuth 2.0 — The Enterprise Option

OAuth 2.0 is an authorization framework — and that distinction matters. It's designed to let third-party applications request limited access to a user's account without exposing their credentials. Think "Sign in with Google" or "Connect your GitHub account."

For WordPress, OAuth 2.0 requires an additional plugin like WP OAuth Server or a custom implementation. There's no built-in OAuth support in WordPress core.

The typical OAuth flow for a headless WordPress setup looks like this:

  1. Frontend redirects user to WordPress authorization endpoint
  2. User logs in on WordPress and approves the application
  3. WordPress redirects back to the frontend with an authorization code
  4. Frontend exchanges the code for an access token (server-side, for security)
  5. Frontend uses the access token to make API requests

That's a lot of redirects for a headless app. If your frontend is a React SPA or a mobile app, the redirect-based flow creates a jarring user experience — you're bouncing the user to your WordPress backend to log in, then back to your app. It works, but it feels like 2012.

OAuth shines when you're building a platform where third-party developers need delegated access to your users' data. If you're building the next Zapier integration or a marketplace of apps that connect to WordPress — OAuth is the right tool. It's built for that exact problem.

For a standard headless WordPress site where you control both the frontend and the backend? OAuth adds complexity without solving a problem that JWT doesn't already handle better.

I get asked about adding OAuth to JWT Auth Pro occasionally. Honestly, I don't see the need for most WordPress projects. OAuth solves a specific problem — delegated third-party access — and if you don't have that problem, you're paying the complexity tax for nothing. To be fair to Application Passwords, WordPress does gate them behind HTTPS or local environments by default and makes them individually revocable. I still prefer JWT when I want short-lived tokens and better session control.

Head-to-Head Comparison

Here's the short version — how the three methods compare on the things that actually matter:

Application PasswordsJWTOAuth 2.0
SetupNone — built into WP 5.6+Install plugin, add configInstall OAuth server, register clients
Credentials per requestYes (every request)No (token only)No (token only)
Token expirationNeverConfigurableConfigurable
Token refreshN/AYes (JWT Auth Pro)Depends on flow/server
Session revocationRevoke one app passwordRevoke specific tokenRevoke grant
Cross-origin frontend fitWorks, but still needs CORSBetter fit, still needs CORSBetter fit, still needs CORS
Plugin requiredNoYesYes
Mobile-friendlyWorks, but credentials on deviceDesigned for itRedirect flow is awkward
Management dashboardNoneYes (JWT Auth Pro)Depends on implementation
ComplexityLowMediumHigh

My Recommendation

I built a JWT plugin, so obviously I'm going to say "use JWT." Right?

Well, yes — but not for the reason you'd expect. I don't recommend JWT because I built it. I recommend it because after watching thousands of developers authenticate headless WordPress apps over the past decade, JWT hits the sweet spot for most real-world use cases I see.

Here's my honest decision framework:

Use Application Passwords when you're building a quick prototype, an internal admin tool, a CI/CD script, or any integration where you control both sides and don't need token management. It's built into core, it works in minutes, and for simple use cases the "credentials on every request" trade-off doesn't matter much.

Use JWT when you're building a production headless app — whether that's Next.js, React, Vue, a mobile app, or anything where real users are authenticating. You get token expiration, refresh tokens (with Pro), session management, and the peace of mind that a compromised token expires instead of living forever. Many headless WordPress projects land here.

Use OAuth when you're building a platform with third-party developers who need delegated access to your users' WordPress data. If nobody outside your team will ever authenticate against your WordPress API, you don't need OAuth.

In my own inbox, the setups I hear about most are React or Next.js frontends, mobile apps, and backend-to-backend integrations. That's anecdotal, not a formal market survey, but it's enough to show where the pain points repeat.

Common Authentication Gotchas

Regardless of which method you choose, these issues will bite you. I've seen every one of them in support tickets.

Apache Strips the Authorization Header

This is one of the most common headless WordPress authentication failures I see. On some Apache setups, the Authorization header never reaches WordPress unless you explicitly pass it through.

A common fix is to add this to your .htaccess before the WordPress rewrite rules:

.htaccess
# Pass the Authorization header to WordPress
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

On Nginx, a common fix is to pass the header in your server block:

nginx.conf (server block)
# Ensure Authorization header reaches PHP
fastcgi_param HTTP_AUTHORIZATION $http_authorization;

This comes up constantly on shared hosting and self-managed Apache stacks. Hosting behavior varies, so test it on your actual environment instead of assuming your provider already forwards the header.

CORS Preflight Fails

When your frontend and WordPress are on different domains, the browser sends a preflight OPTIONS request before the actual request. If WordPress doesn't respond to that preflight correctly, your authenticated request never fires.

You need to handle CORS headers in WordPress. JWT Auth Pro handles this, but if you're using Application Passwords or want to customize it:

functions.php or a custom plugin
add_action('rest_api_init', function () {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function ($value) {
        $origin = get_http_origin();
        $allowed_origins = [
            'https://app.example.com',
            'http://localhost:3000',
        ];

        if (in_array($origin, $allowed_origins, true)) {
            header("Access-Control-Allow-Origin: {$origin}");
            header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
            header('Access-Control-Allow-Headers: Authorization, Content-Type');
            header('Access-Control-Allow-Credentials: true');
        }

        return $value;
    });
}, 15);

Token Storage on the Client

Where you store the token matters more than most developers realize.

localStorage is accessible to any JavaScript on the page. If your site has an XSS vulnerability, an attacker can read the token. Convenient, but risky.

httpOnly cookies aren't accessible to JavaScript. Safer against XSS, but you're back to dealing with CORS and cookie policies across domains.

In-memory (a variable in your app state) is the safest option, but the token disappears on page refresh. You'll need a refresh mechanism to make this work.

For most headless WordPress setups, I'd store the access token in memory and the refresh token in an httpOnly cookie. The access token lives for a short time (15-30 minutes), so losing it on refresh isn't a big deal — the refresh token gets you a new one.

WordPress Behind a Reverse Proxy

If WordPress sits behind Cloudflare, a load balancer, or any reverse proxy, verify that the Authorization header survives every hop. Some stacks pass it through already; custom proxy layers often need explicit forwarding rules.

The symptom: authentication works on your local dev environment but fails in production. Every time.


Headless WordPress authentication isn't hard once you pick the right method and know the gotchas. For most production projects, JWT gives you the best balance of security, flexibility, and developer experience. Application Passwords are great for quick integrations. OAuth is there if you genuinely need delegated third-party access.

If you're building a headless WordPress project and want JWT authentication with token management, refresh tokens, and an admin dashboard to see what's happening, check out JWT Auth Pro.

For the basics, the free JWT Authentication plugin on WordPress.org has you covered — it's been doing the job since 2015 for 60,000+ sites and counting.