DEV Community

loading...
Cover image for How To Use WordPress As An Authentication Provider For Laravel

How To Use WordPress As An Authentication Provider For Laravel

shelob9 profile image Josh Pollock ・7 min read

Like a lot of PHP and JavaScript developers, I started web development by learning enough PHP and then jQuery to customize a WordPress theme and then got carried away from there. Along the way, I learned what WordPress is a good solution for and what it is not a great fit for. This has led to learning Angular, Vue, React and Laravel.

Overtime, more and more of my work has been building WordPress-adjacent apps in Laravel and/ or Node. This has led me to a variety of solutions for sharing data and user authentication between the two. This article shows how to allow users of an existing WordPress site to login to a Laravel site with the same username and password. I did this without data duplication -- besides the cache -- or storing passwords anywhere.

If your WordPress and Laravel applications are able to access the same database, there is an alternative solution, that I will discuss later. The solution I will share in this post, uses the WordPress REST API to check credentials and issue JWT tokens for users.

Setting Up The WordPress Site

This solution requires a WordPress site that has the JWT authentication plugin. This is a free plugin you can install through wp-admin.

Once that plugin is activated, make sure to set a long, random string in the constant JWT_AUTH_SECRET_KEY. Also, ensure that your server does not block the HTTP Authorization header.

This plugin does use username and password to issue the token. They are not stored, so this process is as relatively secure as long as it is run on HTTPS.

When this plugin is installed, you can make a POST request to the "/jwt-auth/v1/token" endpoint with username and password in the body to get a JWT token. That token can then be set in the Authorization header to authorize requests to the WordPress REST API. If we supply that header when making a GET request to "/wp/v2/users/me" we will get details of the current user.

WordPress 5.6 will add an "application password" feature. This is a basic authentication system that uses a different password then the one for login. In the future, that might be a better solution then JWT tokens.

I followed instructions on how to create a customer user provider. It takes the supplied username and password and attempts to get a JWT token from the WordPress site. If that works, a User model is created -- in memory and cache. The password is not saved. A standard Laravel session is created. That session will be maintained as long as the user remains in the cache or the user logs out.

In this post, I will show you how it works. I also put the source for the app I built to test this on the Githubs. Feel free to fork it. It's a basic Laravel 8 site with Breeze.

Creating An API Client

The first step in the Laravel app will be to create an API client. This client, which will make use of Laravel's HTTP facade will need to be able to make requests with and without the authentication token.

This client will need methods for GET and POST requests, that optionally add the header. I also added a method for exchanging username and password for the token:

<?php

namespace App\Http\Clients;


use Illuminate\Support\Facades\Http;

/**
 * API client for WordPress REST API
 *
 * Must have the JWT auth plugin
 * https://wordpress.org/plugins/jwt-auth/
 */
class WordPressApiClient
{

    public string $apiUrl;
    protected $token;

    protected array $options;

    public function __construct(string $apiUrl, ?string $token = null)
    {
        $this->apiUrl = $apiUrl;
        if ($token) {
            $this->token = $token;
        }
        //In production require TLS on both ends.
        //Else do not verify.
        $this->options = [
            'verify' => 'production' === app('ENV')
        ];

    }

    public function setToken(string $token) : WordPressApiClient
    {
        $this->token = $token;
        return $this;
    }

    public function hasToken(): bool
    {
        return isset($this->token) && is_string($this->token);
    }

    public function get(string $endpoint, array $query = [])
    {
        $url = $this->apiUrl . $endpoint;
        if( $this->hasToken() ){
            $request = Http::withToken($this->token)
                ->withOptions($this->options);
        }else{
            $request = Http::withOptions($this->options);
        }

        return $request
            ->get(
                $url,
                $query
            );

    }

    public function post(string $endpoint, array $body)
    {
        $url = $this->apiUrl . $endpoint;
        if( $this->hasToken() ){
            $request = Http::withToken($this->token)
                ->withOptions($this->options);
        }else{
            $request = Http::withOptions($this->options);

        }
        return $request
            ->post(
                $url,
                $body
            );

    }


    public function getToken(string $username, string $password)
    {
        return $this->post('/jwt-auth/v1/token', [
            'username' => $username,
            'password' => $password
        ]);

    }

}

Enter fullscreen mode Exit fullscreen mode

Creating A User Provider

So far we have an HTTP client that could be used for a user provider. It could also be used to get or edit posts or other data from the WordPress site. For now, we'll look at the user provider.

The documentation for [creating a custom user provider](followed instructions is pretty complete, and worth reading through.

The most important method here is retrieveById(). Returning a user model here sets the current logged in user. I wanted to avoid having to check the token against the WordPress REST API on every request.

To avoid this, I created a factory for User models that caches the results:

{
    protected function userFactory(array $data) : User
    {
       $user =  new User;
       Cache::put(
           $this->cacheKey(
               $user->getAuthIdentifier()
           ),
           $user->toArray(),
           9000
       );
       return  $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

Here is the method that finds users by id, which uses that factory:


    public function retrieveById($identifier)
    {
        //Has user in cache?
        $user = Cache::get(
            $this->cacheKey($identifier)
        );
        //Yes? Make a user model and return it
        if( $user ){
            return (new User())
                ->forceFill($user);
        }

        //No user? Do a login.
        //Could trigger a redirect to wp-logint that comes back with a JWT...
    }
Enter fullscreen mode Exit fullscreen mode

This function, may not return a user model. This happens when there is no no logged in user. IF that happens, thes enext two methods will be called, in the order you see here. If the token check throws an exception, which the API client will do if it's invalid, that will trigger a login error:

public function retrieveByCredentials(array $credentials)
    {
        $r = $this->wordpressClient
            ->getToken(
                $credentials['email'], $credentials['password']
            );
        return $this
            ->userFactory($r);
    }

    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        Auth::setUser($user);
        return $this
            ->retrieveById(
                $user->id
            );
    }
Enter fullscreen mode Exit fullscreen mode

I did modify the User model to simplify it, and make it work for my needs. You don't have to use the User model, you can use any model. That might be useful if you wanted to have users in the Laravel database and users in the WordPress database. Here is my user model:

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'token'
    ];

}
Enter fullscreen mode Exit fullscreen mode

I also deleted the user migration. In my test site I used sqlite for the session and cache table. If I deployed this with Vapor, I would use DynamoDB and/ or Redis instead. That means that the Laravel application may not need a MySQL database, which would be great for scaling and cost-control.

Here is the full user provider:

<?php

namespace App\Providers;

use App\DTO\UserResponse;
use App\Http\Clients\WordPressApiClient;
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;

class WordPressUserProvider  implements  UserProvider
{

    protected WordPressApiClient $wordpressClient;

    public function __construct()
    {
        $this->wordpressClient = new WordPressApiClient(
            env('WP_API_URL')
        );
    }

    public function getModel()
    {
        return User::class;
    }

    protected function cacheKey($identifier):string {
        return __CLASS__ . $identifier;
    }

    public function retrieveById($identifier)
    {
        //Has user in cache?
        $user = Cache::get(
            $this->cacheKey($identifier)
        );
        //Yes? Make a user model and return it
        if( $user ){
            return (new User())
                ->forceFill($user);
        }

        //If nothing is returned here, login redirect will be triggered
        //That is normal for unauthorized users.
    }

    public function retrieveByToken($identifier, $token)
    {
        //Try to find by id
        $user = $this
            ->retrieveById($identifier);

        //Not found? Exchange token for user details again.
        if( ! $user ){
            $r = $this
                ->wordpressClient
                ->get('/wp-json/wp/v2/me');
            return $this
                ->userFactory($r);
        }
        return $user;
    }

    public function updateRememberToken(Authenticatable $user, $token)
    {
        //Not actually needed
    }

    /**
     * @param array $credentials
     * @return User|Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        $r = $this->wordpressClient
            ->getToken(
                $credentials['email'], $credentials['password']
            );
        return $this
            ->userFactory($r);
    }

    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        //Recheck token?
        //Auth::setUser($user);
       return $this
           ->retrieveById(
               $user->id
           );
    }

    protected function userFactory(array $data) : User
    {
        $user =  ( new UserResponse(
            Arr::only($data,[
                'token',
                'ID',
                'user_email'
            ])
        ) )
            ->toModel();
       Cache::put(
           $this->cacheKey(
               $user->getAuthIdentifier()
           ),
           $user->toArray(),
           9000
       );
       return  $user;
    }

}

{
    protected function userFactory(array $data) : User
    {
        $user =  new User;
        $user->forceFill(
          [
              'id' => $data['ID'],
              'name' => $data['user_email'],
              'token' => $data['token']
          ]
        );

        Cache::put(
            $this->cacheKey(
                $user->getAuthIdentifier()
            ),
            $user->toArray(),
            9000
        );
        return  $user;
    }
}

Enter fullscreen mode Exit fullscreen mode

Configuring Everything

That's basically it. The last step is to wire things up. These steps are straight out of the documentation.

First in the AuthProvider service provider, register the user provider:

 Auth::provider('wordpress', function ($app, array $config) {
            return new WordPressUserProvider();
        });
Enter fullscreen mode Exit fullscreen mode

Once that is done, you can tell Laravel to use that provider by modifying config/auth.php.

'providers' => [
    'users' => [
        'driver' => 'wordpress',
    ],
],
Enter fullscreen mode Exit fullscreen mode

In the user provider, I used the env varibale WP_API_URL for the URL of the site's REST API. You would put something like WP_API_URL=https://hiroy.club/wp-json in your .env file.

Conclusion

As an alternative to the approach I showed in this post, you may want to look at this package. That package requires access to WordPress' MySQL database. That's not required by the approach I showed here.

My organization's goal is to decouple our big WordPress site from our other internal applications, so co-location didn't make sense. Also, I want to use managed WordPress hosting providers for WordPress and Vapor for Laravel. Mixing them on the same server feels like a regression.

I hope you found this useful. Please feel free to copy the example application that is on Github. I am curious to see if anyone finds this useful and if you have better solutions. I know one thing I'm considering is using a redirect to the WordPress login screen.

Discussion (1)

pic
Editor guide
Collapse
piojo1991 profile image
Piojo1991 • Edited

hello i can't deploy the app. It is connected to JWT but gives the following error

Spatie\DataTransferObject\DataTransferObjectError
The following invalid types were encountered: expected App\DTO\UserResponse::ID to be of type integer, instead got value null, which is NULL. expected App\DTO\UserResponse::token to be of type string, instead got value null, which is NULL. expected App\DTO\UserResponse::user_email to be of type string, instead got value null, which is NULL.

thanks