Skip to main content
WP HealthKit

WordPress JWT Authentication: Custom REST Security

July 11, 202615 min readSecurityBy Jamie

Table of Contents

Securing your WordPress REST API endpoints is critical, and JWT authentication for WordPress REST API has become increasingly popular for building modern, stateless applications. However, implementing JWT securely requires careful consideration of token generation, validation, storage, and refresh strategies. This guide walks you through JWT implementation, compares it with OAuth2, and reveals common security pitfalls that WP HealthKit helps teams identify and fix.

Understanding JWT in WordPress Context

JSON Web Tokens (JWT) are URL-safe, stateless tokens that encode claims about a user. Unlike traditional session-based authentication where the server maintains session state, JWT authentication is stateless—the server doesn't need to store anything about the token.

A JWT consists of three parts separated by dots: header.payload.signature. The header specifies the token type and hashing algorithm. The payload contains claims—assertions about the user, such as their ID, email, and permissions. The signature ensures the token hasn't been tampered with.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE2NzY3MzQ1NzB9.
4u5oQ9w3K4b8c6d2e7f9a1b3c5d7e9f1g2h4i6j8k0l

WordPress plugins like WP REST Authentication and Simple JWT Authentication have made implementing JWT more accessible. However, WP HealthKit's analysis shows that many implementations contain subtle security vulnerabilities that compromise the entire system.

JWT vs OAuth2: Choosing the Right Approach

JWT and OAuth2 are often confused, but they solve different problems. Understanding the distinction helps you choose the right approach for your needs.

JWT is a token format—a way to encode information. OAuth2 is an authorization framework that specifies how to request, issue, and use tokens. OAuth2 can use JWT as its token format, but it doesn't have to.

JWT Characteristics:

  • Stateless (server doesn't store token data)
  • Self-contained (all claims are in the token)
  • Simple to implement
  • Good for first-party applications
  • Difficult to revoke tokens immediately
  • Suitable for mobile and single-page applications

OAuth2 Characteristics:

  • Delegation protocol (third-party services)
  • Server-issued tokens
  • Tokens can be revoked immediately
  • Supports multiple authorization grant types
  • More complex to implement correctly
  • Industry standard for third-party integrations

For WordPress plugins and custom integrations with your own applications, JWT often provides sufficient security with simpler implementation. For building an ecosystem where third-party apps request access to user data, OAuth2 is the better choice.

WP HealthKit recommends JWT for internal API authentication and OAuth2 for public API ecosystems, helping teams implement both securely.

Implementing JWT Authentication in WordPress

Here's a production-ready JWT authentication implementation for WordPress:

// Register JWT authentication endpoint
add_action('rest_api_init', function() {
    register_rest_route('wp-healthkit/v1', '/auth/token', array(
        'methods' => 'POST',
        'callback' => 'healthkit_generate_jwt_token',
        'permission_callback' => '__return_true',
        'args' => array(
            'username' => array(
                'type' => 'string',
                'required' => true,
            ),
            'password' => array(
                'type' => 'string',
                'required' => true,
            ),
        ),
    ));
});

function healthkit_generate_jwt_token($request) {
    $username = $request->get_param('username');
    $password = $request->get_param('password');
    
    // Authenticate user
    $user = wp_authenticate($username, $password);
    
    if (is_wp_error($user)) {
        return new WP_Error(
            'invalid_credentials',
            'Invalid username or password',
            array('status' => 401)
        );
    }
    
    // Generate JWT token
    $token = healthkit_create_jwt_token($user->ID);
    
    return new WP_REST_Response(array(
        'token' => $token,
        'expires_in' => 3600,
        'token_type' => 'Bearer',
    ), 200);
}

function healthkit_create_jwt_token($user_id) {
    // Token configuration
    $issue_time = time();
    $expire_time = $issue_time + 3600; // 1 hour expiration
    $secret_key = defined('HEALTHKIT_JWT_SECRET') ? 
        HEALTHKIT_JWT_SECRET : 
        AUTH_KEY . AUTH_SALT;
    
    // Create payload
    $payload = array(
        'iss' => get_bloginfo('url'),
        'iat' => $issue_time,
        'exp' => $expire_time,
        'sub' => $user_id,
        'user_id' => $user_id,
    );
    
    // Get user data to include in token
    $user = get_user_by('ID', $user_id);
    if ($user) {
        $payload['user_login'] = $user->user_login;
        $payload['user_email'] = $user->user_email;
        $payload['caps'] = array_keys($user->caps);
    }
    
    // Encode header
    $header = array(
        'alg' => 'HS256',
        'typ' => 'JWT',
    );
    
    $header_encoded = healthkit_base64url_encode(
        wp_json_encode($header)
    );
    $payload_encoded = healthkit_base64url_encode(
        wp_json_encode($payload)
    );
    
    // Create signature
    $signature = healthkit_base64url_encode(
        hash_hmac(
            'sha256',
            $header_encoded . '.' . $payload_encoded,
            $secret_key,
            true
        )
    );
    
    return $header_encoded . '.' . $payload_encoded . '.' . $signature;
}

function healthkit_base64url_encode($string) {
    return str_replace(
        array('+', '/', '='),
        array('-', '_', ''),
        base64_encode($string)
    );
}

function healthkit_base64url_decode($string) {
    $padding = 4 - (strlen($string) % 4);
    if ($padding !== 4) {
        $string .= str_repeat('=', $padding);
    }
    
    return base64_decode(
        str_replace(array('-', '_'), array('+', '/'), $string)
    );
}

This implementation creates JWT tokens with appropriate headers, payload data, and HMAC-SHA256 signatures. The token expires after one hour, requiring clients to refresh periodically.

Validating JWT Tokens in REST Endpoints

Validating JWT tokens is the other side of the authentication equation. You need to verify token signatures and check expiration before granting access to protected endpoints:

function healthkit_validate_jwt_token($token) {
    // Extract token from Authorization header if needed
    if (!$token && isset($_SERVER['HTTP_AUTHORIZATION'])) {
        $auth_header = $_SERVER['HTTP_AUTHORIZATION'];
        if (preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) {
            $token = $matches[1];
        }
    }
    
    if (!$token) {
        return false;
    }
    
    // Split token into parts
    $parts = explode('.', $token);
    if (count($parts) !== 3) {
        return false;
    }
    
    list($header_encoded, $payload_encoded, $signature_encoded) = $parts;
    
    $secret_key = defined('HEALTHKIT_JWT_SECRET') ? 
        HEALTHKIT_JWT_SECRET : 
        AUTH_KEY . AUTH_SALT;
    
    // Verify signature
    $expected_signature = healthkit_base64url_encode(
        hash_hmac(
            'sha256',
            $header_encoded . '.' . $payload_encoded,
            $secret_key,
            true
        )
    );
    
    if (!hash_equals($signature_encoded, $expected_signature)) {
        return false;
    }
    
    // Decode and verify payload
    $payload_json = healthkit_base64url_decode($payload_encoded);
    $payload = json_decode($payload_json, true);
    
    if (!$payload) {
        return false;
    }
    
    // Check expiration
    $current_time = time();
    if (!isset($payload['exp']) || $payload['exp'] < $current_time) {
        return false;
    }
    
    // Verify issuer
    if (!isset($payload['iss']) || $payload['iss'] !== get_bloginfo('url')) {
        return false;
    }
    
    return $payload;
}

// Register a simple JWT-protected endpoint
add_action('rest_api_init', function() {
    register_rest_route('wp-healthkit/v1', '/protected/profile', array(
        'methods' => 'GET',
        'callback' => 'healthkit_get_user_profile',
        'permission_callback' => function() {
            $payload = healthkit_validate_jwt_token();
            return !empty($payload);
        },
    ));
});

function healthkit_get_user_profile() {
    $payload = healthkit_validate_jwt_token();
    
    if (!$payload) {
        return new WP_Error(
            'invalid_token',
            'Invalid or expired token',
            array('status' => 401)
        );
    }
    
    $user = get_user_by('ID', $payload['user_id']);
    
    if (!$user) {
        return new WP_Error(
            'user_not_found',
            'User not found',
            array('status' => 404)
        );
    }
    
    return new WP_REST_Response(array(
        'id' => $user->ID,
        'username' => $user->user_login,
        'email' => $user->user_email,
    ), 200);
}

This validation function extracts tokens from the Authorization header, verifies the signature using the same secret key, and checks token expiration and issuer. It uses hash_equals() to prevent timing attacks when comparing signatures.

Token Refresh Strategies

JWT tokens have fixed expiration times for security reasons—if a token is compromised, it's only valid for a limited window. However, requiring users to log in every hour is poor UX. Token refresh strategies solve this problem:

// Endpoint for refreshing tokens
add_action('rest_api_init', function() {
    register_rest_route('wp-healthkit/v1', '/auth/refresh', array(
        'methods' => 'POST',
        'callback' => 'healthkit_refresh_jwt_token',
        'permission_callback' => '__return_true',
        'args' => array(
            'refresh_token' => array(
                'type' => 'string',
                'required' => true,
            ),
        ),
    ));
});

function healthkit_refresh_jwt_token($request) {
    $refresh_token = $request->get_param('refresh_token');
    
    // Validate refresh token (longer expiration time)
    $payload = healthkit_validate_refresh_token($refresh_token);
    
    if (!$payload) {
        return new WP_Error(
            'invalid_refresh_token',
            'Invalid or expired refresh token',
            array('status' => 401)
        );
    }
    
    // Create new access token
    $new_token = healthkit_create_jwt_token($payload['user_id']);
    
    return new WP_REST_Response(array(
        'token' => $new_token,
        'expires_in' => 3600,
        'token_type' => 'Bearer',
    ), 200);
}

function healthkit_create_refresh_token($user_id) {
    $issue_time = time();
    $expire_time = $issue_time + (7 * 24 * 60 * 60); // 7 days
    $secret_key = defined('HEALTHKIT_JWT_SECRET') ? 
        HEALTHKIT_JWT_SECRET : 
        AUTH_KEY . AUTH_SALT;
    
    $payload = array(
        'iss' => get_bloginfo('url'),
        'iat' => $issue_time,
        'exp' => $expire_time,
        'sub' => $user_id,
        'user_id' => $user_id,
        'type' => 'refresh',
    );
    
    $header = array(
        'alg' => 'HS256',
        'typ' => 'JWT',
    );
    
    $header_encoded = healthkit_base64url_encode(
        wp_json_encode($header)
    );
    $payload_encoded = healthkit_base64url_encode(
        wp_json_encode($payload)
    );
    
    $signature = healthkit_base64url_encode(
        hash_hmac(
            'sha256',
            $header_encoded . '.' . $payload_encoded,
            $secret_key,
            true
        )
    );
    
    return $header_encoded . '.' . $payload_encoded . '.' . $signature;
}

function healthkit_validate_refresh_token($token) {
    $payload = healthkit_validate_jwt_token($token);
    
    if (!$payload) {
        return false;
    }
    
    // Refresh tokens must have type claim set to 'refresh'
    if (!isset($payload['type']) || $payload['type'] !== 'refresh') {
        return false;
    }
    
    return $payload;
}

With this refresh token system, clients receive a short-lived access token and a longer-lived refresh token. When the access token expires, the client uses the refresh token to obtain a new access token without requiring the user to log in again.

Common JWT Security Pitfalls

JWT implementations in WordPress frequently contain vulnerabilities. Understanding these pitfalls helps you build secure systems that WP HealthKit's security audits are specifically designed to catch.

Weak Secret Keys: Using predictable secret keys like default WordPress salts allows attackers to forge tokens. Always use cryptographically strong, random secret keys. Define HEALTHKIT_JWT_SECRET in wp-config.php using a strong random value generated by your hosting provider or a service like openssl rand -base64 32.

No Signature Verification: Some implementations skip signature verification entirely, trusting that tokens are valid if they can be decoded. This allows trivial token forgery. Always verify signatures using the same algorithm and key.

Algorithm Confusion Attacks: Never allow clients to specify the algorithm for signing tokens. Some implementations use HS256 by default but accept RS256 if the client requests it, which can lead to authentication bypass. Always specify the algorithm yourself.

Missing Expiration Checks: Tokens without expiration times remain valid forever. Even if a token is compromised, it grants permanent access. Always include and verify the exp claim.

Storing Sensitive Data in Tokens: Never put passwords, API keys, or other sensitive data in JWT payloads. Tokens are often logged, stored in browser history, and visible in network traffic. Include only non-sensitive claims like user ID and roles.

Inadequate Refresh Token Security: Refresh tokens are more valuable than access tokens because they last longer. Store them securely in HTTP-only cookies, not localStorage. Implement refresh token rotation where old tokens are invalidated after use.

Token Revocation Gaps: Because JWT tokens are stateless, revoking a token before expiration is difficult. Implement a token blacklist using transients or options to immediately revoke tokens when users log out or change permissions.

Implementing Token Revocation

// Revoke token on logout
add_action('rest_insert_user', function($user) {
    // Store revoked token timestamp
    update_user_meta($user->ID, 'jwt_revoke_before', time());
});

function healthkit_validate_jwt_token($token) {
    $payload = healthkit_validate_jwt_token($token);
    
    if (!$payload) {
        return false;
    }
    
    // Check if token was issued before revocation
    $revoke_before = get_user_meta($payload['user_id'], 'jwt_revoke_before', true);
    
    if ($revoke_before && $payload['iat'] < $revoke_before) {
        return false;
    }
    
    return $payload;
}

Token revocation ensures that immediately upon logout or permission changes, tokens become invalid.

JWT in Client Applications

Client applications must handle JWT tokens properly. Never store tokens in localStorage where they're vulnerable to XSS attacks:

// Request access token
async function login(username, password) {
    const response = await fetch('/wp-json/wp-healthkit/v1/auth/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
        credentials: 'include',
    });
    
    const data = await response.json();
    
    // Store in memory and HTTP-only cookie
    sessionStorage.setItem('access_token', data.token);
    
    return data.token;
}

// Request with JWT token
async function fetchAPI(endpoint) {
    const token = sessionStorage.getItem('access_token');
    
    const response = await fetch(endpoint, {
        headers: {
            'Authorization': `Bearer ${token}`,
        },
        credentials: 'include',
    });
    
    return response.json();
}

// Refresh token when expired
async function refreshAccessToken(refreshToken) {
    const response = await fetch('/wp-json/wp-healthkit/v1/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refresh_token: refreshToken }),
        credentials: 'include',
    });
    
    const data = await response.json();
    sessionStorage.setItem('access_token', data.token);
    
    return data.token;
}

This approach stores tokens in memory for the current session, reducing XSS impact. HTTP-only cookies contain refresh tokens that JavaScript can't access, protecting them from XSS attacks.

Additional Resources

Broader Context and Best Practices

Security vulnerabilities in WordPress plugins don't exist in isolation. Each vulnerability represents a potential entry point that attackers chain together to achieve broader compromise. A seemingly minor issue like improper input validation can escalate when combined with a privilege escalation flaw, turning a low-severity finding into a critical breach. This interconnected nature of security weaknesses is why comprehensive auditing matters so much. Rather than checking individual items in isolation, modern security analysis examines how different components interact and where those interactions create unexpected attack surfaces that manual review would miss entirely.

The WordPress plugin ecosystem's open-source nature creates both strengths and challenges for security. Open code allows community review, which catches many issues early. However, it also means attackers can study source code to find exploitable patterns before patches are released. This asymmetry makes proactive security testing essential rather than reactive. Developers who integrate automated security scanning into their development workflow catch vulnerabilities during development, long before code reaches production. The cost of fixing a security issue during development is orders of magnitude lower than addressing it after a public disclosure or active exploitation.

Understanding the attacker's perspective transforms how developers approach security. Attackers don't think in terms of individual functions or classes. They think in terms of data flows, trust boundaries, and privilege transitions. When data crosses from an untrusted context like user input into a trusted context like a database query, that boundary is where vulnerabilities emerge. By mapping these trust boundaries in your plugin architecture, you can systematically identify where validation, sanitization, and authorization checks are needed.

WordPress powers over forty percent of the web, making it the single largest target for automated attacks. Plugin vulnerabilities are the primary vector for these attacks, with Patchstack reporting thousands of new plugin vulnerabilities each year. The scale of the WordPress ecosystem means that even a vulnerability affecting a relatively obscure plugin can impact hundreds of thousands of sites. This reality underscores why every plugin developer has a responsibility to take security seriously.

Broader Industry Context and Best Practices

Security hardening in WordPress extends beyond individual plugin fixes to encompass a holistic defense strategy. Organizations managing multiple WordPress installations benefit from centralized security policies that enforce consistent standards across all sites. This includes automated vulnerability scanning, real-time threat intelligence feeds, and coordinated patch management. WP HealthKit provides the automated scanning infrastructure that makes centralized security monitoring practical, giving teams visibility into vulnerabilities across their entire WordPress portfolio. Regular security assessments should evaluate not just known vulnerabilities but also configuration drift, where settings gradually deviate from security baselines over time, creating subtle but exploitable weaknesses.

The WordPress security landscape continues evolving as attackers develop increasingly sophisticated techniques. Supply chain attacks targeting plugin update mechanisms, zero-day exploits in popular themes, and credential stuffing campaigns against wp-admin endpoints represent growing threat vectors. Effective defense requires layered security controls: web application firewalls filter malicious requests, file integrity monitoring detects unauthorized changes, and behavioral analysis identifies anomalous patterns. WP HealthKit scans for these vulnerability patterns automatically, helping teams stay ahead of emerging threats. Security teams should also implement network segmentation to limit lateral movement if an attacker compromises a single WordPress instance within a larger infrastructure.

Compliance requirements add another dimension to WordPress security planning. Organizations in regulated industries must demonstrate that their WordPress deployments meet specific security standards, whether PCI DSS for payment processing, HIPAA for healthcare data, or SOC 2 for service providers. This means maintaining detailed audit trails, implementing access controls with principle of least privilege, and conducting regular penetration testing. WP HealthKit audit reports provide documentation that supports compliance evidence gathering, making it easier to demonstrate security due diligence during audits. Automated compliance checking reduces the manual effort required for audit preparation while ensuring continuous adherence to security requirements throughout the year.

Frequently Asked Questions

Is JWT safer than traditional session-based authentication?

JWT and sessions provide different security properties. JWT is stateless and scalable, making it ideal for distributed systems. Sessions allow immediate token revocation. Neither is inherently safer—implementation matters more than the approach. WP HealthKit analyzes both approaches to identify vulnerabilities.

Can I use JWT without HTTPS?

No. JWT tokens should never be transmitted over unencrypted connections. Without HTTPS, attackers can intercept tokens and use them to impersonate users. Always require HTTPS for endpoints using JWT authentication.

How do I protect refresh tokens?

Store refresh tokens in HTTP-only cookies that JavaScript can't access. This prevents XSS attacks from stealing them. Implement refresh token rotation where old tokens are invalidated after use, limiting the window if a token is compromised.

What happens if someone gets my secret key?

If the JWT secret key is compromised, attackers can forge arbitrary tokens. Rotate the key immediately by generating a new one and invalidating all existing tokens. Implement a gradual migration where you accept both old and new keys temporarily while distributing the new key.

Should I include user permissions in the JWT payload?

Including permissions in the payload is a tradeoff. It reduces database queries during authorization checks but makes permission changes take time to propagate (until tokens expire). For most applications, this tradeoff is acceptable. For systems requiring immediate permission revocation, validate permissions on each request.

How do I handle token expiration in user experience?

When tokens expire, retry the request with a refreshed token automatically. If refresh fails, redirect to login. This keeps users in a logged-in state while maintaining security through short-lived tokens.

Conclusion

JWT authentication provides a powerful, scalable approach to securing WordPress REST APIs. By understanding the distinction between JWT and OAuth2, implementing proper token generation and validation, designing token refresh strategies, and avoiding common pitfalls, you can build secure REST APIs that scale to modern application demands.

The implementation examples in this guide provide production-ready code that handles token generation, validation, refresh, and revocation. However, security vulnerabilities are subtle and often invisible. Regular security audits are essential for catching implementation flaws before they're exploited.

WP HealthKit automatically analyzes JWT implementations in your plugins and custom code, identifying misconfigurations and vulnerabilities that put your users at risk. Start scanning your WordPress installation today to ensure your JWT implementation is production-ready and secure.

For related security topics, explore our guide on REST API batch processing and custom endpoint security. The OWASP JWT Cheat Sheet provides additional security recommendations from security experts.

Don't leave authentication vulnerabilities to chance. Use WP HealthKit to audit your JWT implementation and ensure your WordPress applications are truly secure.

Ready to audit your plugin?

WP HealthKit checks for all the issues in this article and 40+ more across 49 verification layers.

Comments

WordPress JWT Authentication: Custom REST Security | WP HealthKit