Problems with WordPress JWT Authentication

By , last updated July 2, 2019

While I’m been doing some research for WordPress publishing, I eventually came across JWT authentication. It’s a form of WP REST API authentication, using JSON Web Tokens.

I’ve had a fair share of problems and encumberments in getting JWT auth to work with WordPress.

Some time between this summer and last summer (2017-2018) the basic auth was removed from WordPressPCL. Instead the only option available is JWT Auth through the JWT Auth plugin.

The new and more secure way is not straight forward. I installed the plugin in an existing multisite installation used for testing. I got the C# project running and trying to authenticate against the multisite installation.

The only file edited was wp-config.php to add a define.

Being a C++ programmer just pretending to know C#, I fired up Visual Studio, created a new C# WPF project and added WordPressPCL dependency through NuGet.

define('JWT_AUTH_SECRET_KEY', 'top-secure-salted-phrase');

With the test code that instanciates WordPressClient and creates request JWT token:

try
{
    WordPressPCL.WordPressClient wp = new WordPressPCL.WordPressClient(uri);

    var posts = await wp.Posts.GetPostsByAuthor(1);

    await wp.RequestJWToken(user, pass);

    bool valid = await wp.IsValidJWToken();

    var wpsettings = await wp.GetSettings();
    int babaawd = 0;

    var post = await wp.Posts.Create(new WordPressPCL.Models.Post
    {
        Title = new WordPressPCL.Models.Title("From PCL"),
        Content = new WordPressPCL.Models.Content("From PCL content"),
        Status = WordPressPCL.Models.Status.Draft
    });

}
catch (Exception ex)
{
    return ex.ToString();
}

It always failed on RequestJWToken.

Fail

It failed. Hard.

There were no good error messages, other than a rather unhelpful message telling me it didn’t expect a < character at position 0.

Unexpected character encountered while parsing value: <. Path '', line 0, position 0.

This wasn’t good. Time to fire up Wireshark.

Wireshark

A short capture later with Wireshark, confirmed my suspicion. WordPress was indeed returning HTML instead of JSON.

Wireshark capture

Wireshark capture

cURL

The next step was to actually test the endpoint with cURL.

cURL is a command line tool used to test URLs with various parameters. And it’s simple to change a parameter or two to observe the difference.

curl -X POST -H 'Content-Type: application/json' -d '{"username": "user", "password": "insecure-password"}' http://example.com/wp-json/jwt-auth/v1/token

The results were amazing. I actually returned some valid data.

HTTP/1.1 200 OK
Date: Fri, 01 Jun 2018 20:42:30 GMT
Server: Apache
X-Powered-By: PHP/5.6.29-pl0-gentoo
X-Robots-Tag: noindex
Link: <http://example.com/wp-json/>; rel="https://api.w.org/"
X-Content-Type-Options: nosniff
Access-Control-Expose-Headers: X-WP-Total, X-WP-TotalPages
Access-Control-Allow-Headers: Authorization, Content-Type
Allow: POST
Content-Length: 310
Content-Type: application/json; charset=UTF-8

{"token":"base64token.removed.fromhere","user_email":"user@example.com","user_nicename":"user","user_display_name":"user"}

This was a nudge in the right direction. Time to look closer at the Wireshark capture.

Urlencoded Capture

Urlencoded Capture

Indeed, it sends the application/x-www-form-urlencoded content type when trying to get a JWT token. Why?

Using cURL with application/x-www-form-urlencoded gives the same HTML-page as above, though the reponse content type should have been application/json.

Localhost vs other host

I was stuck a couple of hours trying to reproduce with cURL. The test program was on an other server, while I was testing with cURL on the same host as the web server. Apparently they were being treated differently in WordPress with this issue. From localhost, the cURL approach did not fail. It was always success.

Hacking the sources

I’ll fast forward through the time I downloaded the sources to WordPressPCL and changed the content type from application/x-www-form-urlencoded to application/json. Apparently it fixed the problem.

But it couldn’t be correct this was the intended behaviour?

I was about to create an issue on Github with WordPressPCL. But I had to check one thing first, how does this compare to a clean install of WordPress?

Clean install

Once the clean and fresh instance of WordPress was set up and available on the intranet, it was time to install the JWT Authenticaion plugin (jwt-authentication-for-wp-rest-api).

The only configuration was to add the define JWT_AUTH_SECRET_KEY in wp-config.php.

The first call to the REST API was successful. It was a simple GetPosts() call requiring no authentication.

Hacking JWT Authentication plugin

It’s not really necessary to hack the plugin. In my humble opinion, the PHP fix should have been a part of the library from the beginning.

Both of the following have the same outcome.

Edit .htaccess

The JWT Auth plugin expects either a HTTP_AUTHORIZATION header or a REDIRECT_HTTP_AUTHORIZATION header containing the WordPress authentication token.

What this snippet does is to rewrite the Authorization header into HTTP_AUTHORIZATION, which the JWT Auth plugin understands.

In the root .htaccess file, add:

SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

There is also an option to use rewrite rules. I’ve tested these and I couldn’t see any changes in behaviour.

They may or may not work.

RewriteEngine on
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

PHP JWT fix

When trying to validate a JW Token, it failed. Even though I supplied the correct username and password. There was something amiss. A quick Google search later I came across a Stackoverflow post detailing what could be wrong.

In the file public/class-jwt-auth-public.php on line 235, in method "validate_token", add the following snippet.

/* Triple check for an Authentication header */
if (!$auth) {
    // Look for the actual Authorization header if it haven't been found so far
    $allHeaders = getallheaders();
    $auth = isset($allHeaders['Authorization']) ? $allHeaders['Authorization'] : false;
}

The complete start of the method should now read:

public function validate_token($output = true)
{
    /*
     * Looking for the HTTP_AUTHORIZATION header, if not present just
     * return the user.
     */
    $auth = isset($_SERVER['HTTP_AUTHORIZATION']) ?  $_SERVER['HTTP_AUTHORIZATION'] : false;


    /* Double check for different auth header string (server dependent) */
    if (!$auth) {
        $auth = isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ?  $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : false;
    }

    /* Triple check for an Authentication header */
    if (!$auth) {
        // Look for the actual Authorization header if it haven't been found so far
        $allHeaders = getallheaders();
        $auth = isset($allHeaders['Authorization']) ? $allHeaders['Authorization'] : false;
    }

    if (!$auth) {

At this point, everything was smooth sailing. I was able to authenticate and validate with JWT against the clean installation of WordPress. I was also able to create posts!

Back to the broken installation

With the current libraries working almost as stock, there was certainly something messing with the request. The most probable cause is a plugin. There is no other rational explanation why application/x-www-form-urlencoded fails, while application/json succeeds?

Of about 20 active plugins, there was only two-three plugins who seemed most likely to cause this issue. And then it is just a matter of trial and error. Deactivate, test, activate. And continue to the next plugin.

I got lucky and hit the faulty plugin on the second attempt. It was no other than WP-Spamshield (wp-spamshield). When it got deactivated, the JWT auth request and validate token got through, without fail.

Reading the history behind WP-Spamshield seems somewhat turbulent, where the author behind the plugin is attacking WordPress leadership, and the leadership retaliating against the author. WP-Spamshield got removed from the WordPress plugin directory, and it reappeared on a pay-for-plugins site.

Breaking the clean installation

There is only one way to be sure WP-Spamshield actually was the culprit. The clean installation had to be broken. The plugin sources was copied over and activated.

The test failed.

This means WP-Spamshield intercepts a little too much.

The research for the ideas can continue now.