Adding custom claims to the Laravel Passport JWT

Leo Sjöberg • June 24, 2018

One of the advantages of using JSON Web Tokens as the transport for your authentication layer (such as OAuth) is that you can attach additional data in the form of public claims (see the relevant RFC for more details). However, Laravel Passport does not expose any way to attach such a claim, making it up to us to implement such a solution. This blog post will guide you through how to set that up in the easiest way possible.

Structure

To understand how this will be implemented we first need to have a brief look at how Laravel Passport issues tokens.

Passport uses an AccessToken class responsible for converting the data within the token into its final string form. Thus, in order to change what data is placed in our token, we need to modify how the AccessToken class generates the token. This AccessToken then gets returned to the OAuth Server by an AccessTokenRepository, so we also need to replace the repository to return our own implementation of the AccessToken.

Implementation

AccessToken

The AccessToken class will do the heavy lifting for us – we will override the convertToJWT method to also attach the traits, and in our case also clean up and separate some of Laravel's own code:

1use Lcobucci\JWT\Builder;
2use Lcobucci\JWT\Signer\Key;
3use League\OAuth2\Server\CryptKey;
4use Lcobucci\JWT\Signer\Rsa\Sha256;
5use App\Repositories\User\UserRepository;
6use Laravel\Passport\Bridge\AccessToken as PassportToken;
7 
8class AccessToken extends PassportToken
9{
10 /**
11 * {@inheritdoc}
12 */
13 public function convertToJWT(CryptKey $privateKey)
14 {
15 $customClaims = ['foo' => 'bar'];
16 
17 $tokenBuilder = (new Builder())
18 ->setAudience($this->getClient()->getIdentifier())
19 ->setExpiration($this->getExpiryDateTime()->getTimestamp());
20 return $this->getToken($tokenBuilder, $privateKey, $customClaims);
21 }
22 
23 public function getToken(Builder $builder, CryptKey $privateKey, array $claims = [])
24 {
25 $builder->setId($this->getIdentifier(), true)
26 ->setIssuedAt(time())
27 ->setNotBefore(time())
28 ->setSubject($this->getUserIdentifier())
29 ->set('scopes', $this->getScopes());
30 
31 foreach ($claims as $claim => $value) {
32 $builder->set($claim, $value);
33 }
34 
35 return $builder
36 ->sign(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()))
37 ->getToken();
38 }
39}

In this implementation, we create a new method, getToken to set the claims and retrieve the actual Token class.

The important part is our $customClaims variable. This is where you would retrieve the claims, however you decided to set them. For example, you could use a singleton to store the claims, or store it on the user model and retrieve that, retrieving the ID with $this->getUserIdentifier() – the choice is yours.

Repository

With this done, also create a new AccessTokenRepository class:

1use League\OAuth2\Server\Entities\ClientEntityInterface;
2use Laravel\Passport\Bridge\AccessTokenRepository as BaseRepository;
3 
4class AccessTokenRepository extends BaseRepository
5{
6 /**
7 * {@inheritdoc}
8 */
9 public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
10 {
11 return new AccessToken($userIdentifier, $scopes);
12 }
13}

Make sure that the AccessToken instance returned is of the class we created above.

Service Provider

Last but not least, add the following to your AuthServiceProvider, or another appropriate service provider:

1use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
2use Laravel\Passport\Bridge\AccessTokenRepository as PassportTokenRepository;
3 
4class AuthServiceProvider extends ServiceProvider
5{
6 // ...
7 
8 public function register()
9 {
10 $this->app->bind(PassportTokenRepository::class, AccessTokenRepository::class);
11 }
12}

Conclusion

That's it – we replaced two classes, and added a container binding, and now you can attach custom claims to your Passport JWTs. This is a simple and effective way to attach additional data about your users (for example billing information like trial end date, or email address and name).