A WordPress plugin that completely replaces native authentication with an external JWT provider. All login attempts are redirected to the provider; users are created on demand with the subscriber role. No admin UI — configured entirely via wp-config.php.
Supports two modes:
| Mode | When to use | Examples |
|---|---|---|
| OIDC | WordPress redirects users to the provider for login | Zitadel, Keycloak, Auth0, Okta |
| Proxy | An upstream proxy injects a signed JWT into every request | Cloudflare Zero Trust, Authentik, Traefik Forward Auth |
- PHP 8.4+
- WordPress 6.4+
- Composer
# 1. Place this directory inside wp-content/plugins/jwt-auth/
# 2. Install dependencies
composer install --no-dev --optimize-autoloader
# 3. Activate the plugin in wp-admin, or via WP-CLI:
wp plugin activate jwt-authA Nix devshell with PHP 8.4 and Composer is provided:
nix developAll configuration is done via constants in wp-config.php. The plugin does nothing — and leaves WordPress fully functional — until at least one mode is configured.
Define JWT_AUTH_CLIENT_ID to activate. Endpoints are auto-discovered from {issuer}/.well-known/openid-configuration.
define('JWT_AUTH_ISSUER', 'https://your.zitadel.cloud');
define('JWT_AUTH_CLIENT_ID', 'your-client-id@project');
define('JWT_AUTH_CLIENT_SECRET', ''); // leave empty for PKCE-only (recommended)The plugin uses PKCE (S256) by default. Set JWT_AUTH_CLIENT_SECRET only if your provider requires a confidential client.
Zitadel setup checklist:
- Create a PKCE application in your Zitadel project.
- Set the allowed redirect URI to
https://yoursite.com/?jwt_auth_callback=1. - Set the post-logout redirect URI to
https://yoursite.com/. - Copy the issuer URL and client ID into
wp-config.php.
Define JWT_AUTH_JWKS_URI without JWT_AUTH_CLIENT_ID to activate. The proxy must inject a signed JWT into every authenticated request before it reaches WordPress.
// Cloudflare Zero Trust
define('JWT_AUTH_ISSUER', 'https://yourteam.cloudflareaccess.com');
define('JWT_AUTH_JWKS_URI', 'https://yourteam.cloudflareaccess.com/cdn-cgi/access/certs');
define('JWT_AUTH_AUD', 'your-cf-audience-tag');
define('JWT_AUTH_TOKEN_COOKIE', 'CF_Authorization');
// Or use a header instead of a cookie:
// define('JWT_AUTH_TOKEN_HEADER', 'Cf-Access-Jwt-Assertion');Cloudflare Zero Trust setup checklist:
- Add a Cloudflare Access application protecting your WordPress site.
- Copy the Audience Tag from the application settings into
JWT_AUTH_AUD. - Set your team domain in
JWT_AUTH_ISSUERandJWT_AUTH_JWKS_URIas shown above.
| Constant | Default | Description |
|---|---|---|
JWT_AUTH_ISSUER |
— | Provider base URL. Used for iss claim validation and OIDC discovery. |
JWT_AUTH_CLIENT_ID |
— | OIDC client ID. Presence of this constant activates OIDC mode. |
JWT_AUTH_CLIENT_SECRET |
'' |
OIDC client secret. Leave empty for PKCE-only. |
JWT_AUTH_JWKS_URI |
— | JWKS endpoint URL. Required in proxy mode. Overrides OIDC-discovered URI when set in OIDC mode. |
JWT_AUTH_AUD |
— | Expected aud claim value. Required in proxy mode. Overrides client_id audience check in OIDC mode. |
JWT_AUTH_TOKEN_COOKIE |
— | Cookie name carrying the JWT (proxy mode). |
JWT_AUTH_TOKEN_HEADER |
— | HTTP header name carrying the JWT (proxy mode). Falls back to Authorization: Bearer if neither cookie nor header is configured. |
JWT_AUTH_LOGOUT_URL |
— | Provider logout URL. Overrides OIDC end_session_endpoint when set. |
JWT_AUTH_DEFAULT_ROLE |
subscriber |
WordPress role assigned to newly created users. |
JWT_AUTH_CLAIM_EMAIL |
email |
JWT claim containing the user's email address. |
JWT_AUTH_CLAIM_FIRST_NAME |
given_name |
JWT claim for first name. |
JWT_AUTH_CLAIM_LAST_NAME |
family_name |
JWT claim for last name. |
JWT_AUTH_CLAIM_NAME |
name |
JWT claim for display name. |
JWT_AUTH_REDIRECT |
/ |
Post-login redirect destination. |
JWT_AUTH_PROVIDER_NAME |
SSO |
Provider label shown in the WooCommerce sign-in button. |
- Any visit to
wp-login.phpimmediately redirects to the provider's authorization endpoint. - The plugin generates a random
stateand a PKCEcode_challenge(S256), stored server-side in WordPress transients. Nothing is written to cookies or the URL. - After the user authenticates, the provider redirects to
https://yoursite.com/?jwt_auth_callback=1&code=…&state=…. - The plugin validates the state, exchanges the code for tokens at the provider's token endpoint, and validates the
id_tokenJWT against the provider's JWKS. - A WordPress user is found (by
submeta, then email) or created with the configured default role. - A standard WordPress auth cookie is set and the user is redirected to their original destination.
- The upstream proxy authenticates the user and injects a signed JWT into every request.
- On each unauthenticated WordPress request, the plugin reads the JWT from the configured cookie, header, or
Authorization: Bearer. - If the JWT is valid and the audience matches, the user is found or created and a WordPress session is established for the current and all future requests.
New users are created with:
user_loginset to their email address.- The role defined by
JWT_AUTH_DEFAULT_ROLE(default:subscriber). - The provider's
subclaim stored in user meta asjwt_auth_sub.
On every subsequent login, the user's first name, last name, display name, and email are synced from the JWT claims. The sub meta is used for lookups first, so email changes at the provider are handled gracefully.
The authenticate WordPress filter (priority 1) returns WP_Error for all username/password attempts, including programmatic calls and WooCommerce checkout. WP-CLI and cron jobs are exempt.
On My Account and Checkout pages, a "Sign in with SSO" button is injected:
- Into classic WooCommerce login forms via the
woocommerce_login_form_startPHP hook. - Into block-rendered forms via a small
MutationObserverscript (assets/woo-login.js).
The button is only shown in OIDC mode. In proxy mode, users are automatically authenticated before the page renders.
- CSRF: The OIDC
stateparameter is a 128-bit random value stored in a server-side transient. It is single-use and expires after 10 minutes. - PKCE: The
code_verifieris stored server-side. An intercepted authorization code cannot be exchanged without it. - Open redirect: The post-login
redirect_tovalue is stored server-side in the state transient and validated withwp_validate_redirect()on use. It is never passed through the browser. - JWKS rotation: Keys are cached for 1 hour. A signature validation failure triggers a one-time cache refresh before failing the request, accommodating live key rotation.
- Token replay: WordPress auth cookies provide session continuity. The short-lived ID token (validated once at callback time) is not stored.