Add control to JSON Web Tokens generation on Laravel Passport

Recently I needed to tap on Laravel’s Passport programming to control how the JSON Web Tokens (JWT) were being issued. Specifically, I needed to add more claims to it (to hold more user information) and to control how the scopes were being generated. The idea was to add information like the authenticated user’s email, VAT number, account type, and, also, to forcibly add the scopes that were associated with the user’s role.

However, changing Laravel Passport’s behavior to make it happen isn’t quite obvious, and in a world were service-oriented architectures are becoming ever more common, JWT being the de facto way of carrying user’s information through multiple services, and since I couldn’t find quite a good resource to understand how to do it, I thought I could share with you how I approached and solved my own problem.

The need for control

I’m developing ITsoup, an IT support/helpdesk software system, designed to simplify and streamline all its related processes, based in a service-oriented architecture. This means that I’ll have many services that handle specific domains of work. In order to authenticate the requests, properly, I needed some way of holding users’ information in a way that could be passed around from service to service. The de facto way of doing this is with JWTs, so I started investigating how I could make those changes to the JWT generation logic.

The problem with current Laravel Passport implementation is that it only includes one identification claim for the user: it’s internal ID. That’s not of much use for me on this project. Using only the ID would require any other services to request any additional user information to the Organization Domain service’s API if they need, for example, the e-mail. 

Doing that way would create an unwanted, unnecessary, dependency between this service and all others that work with the user’s data and would increase the load on this Organization Domain service as new services and/or traffic increased.

Understanding Laravel Passport’s Service Provider

Before anything, I like to always understand what’s going on under the hood. By getting a broader context of the system I need to change, I can make informed decisions on how to approach a solution.

As far as I understood, it all goes down on Laravel\Passport\PassportServiceProvider. Here we can find the registerAuthorizationServer() method, which is responsible for defining how Laravel instantiates the AuthorizationServer class and registers the supported oAuth2 authorization grants. But before that happens, there’s a call to a makeAuthorizationServer() method. This method is the one that’s responsible to define how the AuthorizationServer gets instantiated, and injects the proper dependencies from the Laravel’s Container! 

The key class to this whole thing is the Bridge\AccessTokenRepository, which is one of the dependencies injected. Since it’s being injected via the Container, we can take advantage of it and inject our own instance of that class, instead. This class has a very special method, getNewToken(), which is called when an enabled authorization grant needs to generate a new token. 

Hooking to the logic flow, here, and direct it through our own implementation of the AccessTokenRepository class enables us to control how the JWT is created, the information it holds, and many other aspects of it.

Building bridges

So, now that I’ve pinpointed exactly the class that I need to override, I defined that a call to the Bridge\AccessTokenRepository class, within Laravel’s Container, would return an instance of my own AccessTokenRepository class, which would extend the previous one but with the single detail of overriding the getNewToken() method. As of this moment, I successfully routed the logic to my own class.

Now, this method needs to return an implementation of the AccessTokenEntityInterface. This interface defines how to compute an access token (a JWT, for example). Laravel’s Passport implementation is exactly the one that The PHP League’s implements. In fact, not wanting to override that default behavior is one of the main reasons that Passport doesn’t supply any simple solution around this problem. In order for being able to add more claims, and control how JWT is generated – and even the scopes associated – we need to override this exact implementation.

At this point, I just needed to return an implementation of the AccessTokenEntityInterface that suited my needs. There’re two methods, specifically, that I needed to override: convertToJWT() and getScopes(). The first is the one that actually generates the JWT, and the second one is the one that compiles the scopes to associate with that JWT. 

So, basically, my extended convertToJWT() method looked like this:

private function convertToJWT(CryptKey $privateKey): Token
{
    return (new Builder())
        ->permittedFor($this->getClient()->getIdentifier())
        ->identifiedBy($this->getIdentifier())
        ->issuedAt(\time())
        ->canOnlyBeUsedAfter(\time())
        ->expiresAt($this->getExpiryDateTime()->getTimestamp())
        ->relatedTo((string) $this->getUserIdentifier())
        ->withClaim('scopes', $this->getScopes())
        ->withClaim('customer_id', $this->user->customer_id)
        ->withClaim('vat_number', $this->user->vat_number)
        ->withClaim('name', $this->user->name)
        ->withClaim('email', $this->user->email)
        ->withClaim('account_type', $this->user->account_type)
        ->getToken(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()));
}

And my getScopes() method looked like this:

public function getScopes(): array
{
    return $this->user
        ->roles()
        ->pluck('scopes')
        ->flatten()
        ->unique()
        ->map(static function ($scope) {
            return new Scope($scope);
        })
        ->toArray();
}

Now, the JWT is generated with what I defined to be the relevant user information, and can be passed around and, hopefully, reduce the need to query the Organization Domain service if the JWT itself already holds the data needed.