Authentication in a headless WooCommerce setup is fundamentally different from traditional WordPress. There’s no wp-login.php, no cookies managed by PHP, and no nonce verification through theme templates. You need to build an auth layer that works across your decoupled frontend and WordPress backend.
The Authentication Challenge
In traditional WooCommerce, WordPress handles everything: login form, session cookies, nonce tokens, and password reset. When you decouple the frontend, you lose all of this. Your Next.js or Nuxt frontend needs to:
- Authenticate customers against WordPress user accounts
- Maintain session state across page navigations
- Authorize API requests to WooCommerce
- Handle registration, password reset, and account management
Option 1: JWT (JSON Web Tokens)
The most popular approach for headless WordPress authentication.
Setup: Install the [JWT Authentication for WP REST API](https://wordpress.org/plugins/jwt-authentication-for-wp-rest-api/) plugin.
Login flow:
// Frontend: Login
async function login(username, password) {
const response = await fetch(${WP_URL}/wp-json/jwt-auth/v1/token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.token) {
// Store token securely
localStorage.setItem('auth_token', data.token);
return { success: true, user: data.user_display_name };
}
return { success: false, error: data.message };
}
// Frontend: Authenticated API call
async function getMyOrders() {
const token = localStorage.getItem('auth_token');
const response = await fetch(${WP_URL}/wp-json/wc/v3/orders?customer=${userId}, {
headers: { 'Authorization': Bearer ${token} }
});
return response.json();
}
Token validation:
// Validate token is still valid
async function validateToken(token) {
const response = await fetch(${WP_URL}/wp-json/jwt-auth/v1/token/validate, {
method: 'POST',
headers: { 'Authorization': Bearer ${token} }
});
return response.ok;
}
Pros: Stateless, scalable, works across domains
Cons: Token storage security (localStorage is vulnerable to XSS), no built-in refresh mechanism in the standard plugin
Option 2: HttpOnly Cookies with Server-Side Proxy
More secure than client-side JWT storage. Your Next.js API routes proxy auth requests and set HttpOnly cookies.
// Next.js API route: /api/auth/login
export async function POST(request) {
const { username, password } = await request.json();
const wpResponse = await fetch(${WP_URL}/wp-json/jwt-auth/v1/token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await wpResponse.json();
if (data.token) {
const response = NextResponse.json({ success: true, user: data.user_display_name });
response.cookies.set('auth_token', data.token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 60 24 * 7 // 7 days
});
return response;
}
return NextResponse.json({ success: false }, { status: 401 });
}
Pros: Token never exposed to JavaScript, XSS-resistant
Cons: More complex setup, requires server-side rendering or API routes
Option 3: NextAuth.js with WordPress Provider
NextAuth.js provides a complete auth solution with session management, CSRF protection, and multiple provider support.
// app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export const authOptions = {
providers: [
CredentialsProvider({
name: 'WordPress',
credentials: {
username: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
const res = await fetch(${WP_URL}/wp-json/jwt-auth/v1/token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const user = await res.json();
if (res.ok && user.token) {
return {
id: user.user_email,
name: user.user_display_name,
email: user.user_email,
wpToken: user.token
};
}
return null;
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) token.wpToken = user.wpToken;
return token;
},
async session({ session, token }) {
session.wpToken = token.wpToken;
return session;
}
}
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Pros: Battle-tested, handles session management, supports social login providers
Cons: Additional dependency, slight learning curve
Registration
WooCommerce’s REST API supports customer creation:
async function register(email, password, firstName, lastName) {
const response = await fetch(${WP_URL}/wp-json/wc/v3/customers, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Basic ${btoa(${CK}:${CS})} // Consumer key/secret
},
body: JSON.stringify({
email,
password,
first_name: firstName,
last_name: lastName
})
});
return response.json();
}
Note: Customer creation requires consumer key/secret auth (server-side), not customer JWT.
Password Reset
WordPress doesn’t expose password reset via REST API by default. Options:
- Custom endpoint: Build a WordPress plugin that handles reset token generation and password update
- Email-based: Redirect to WordPress’s native password reset page
- Plugin: Use a headless-friendly password reset plugin
Security Best Practices
- Never store JWT in localStorage for production — use HttpOnly cookies or server-side session
- Implement token refresh — JWTs should have short expiry (1 hour) with a refresh mechanism
- Rate limit login attempts — prevent brute force attacks
- Use HTTPS everywhere — tokens in transit must be encrypted
- Validate tokens on every API request — don’t trust client-side state alone
- Implement CORS properly — only allow your frontend domain
Conclusion
For most headless WooCommerce projects, we recommend NextAuth.js with a WordPress credentials provider and HttpOnly cookies. It provides the best balance of security, developer experience, and flexibility. JWT stored in localStorage is fine for prototyping but should be upgraded to server-side session management before production.
Last modified: April 3, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe