Build JWT Authentication Between Multiple API With Laravel

Kalizi <Andrea>
Nov 26, 2020 · 8 min read
Image for post
Image for post
Photo by Priscilla Du Preez on Unsplash

Nowadays building a big system composed of multiple API endpoints is becoming more and more a common practice, even thanks to the diffusion of the microservice pattern.

Adopting this kind of architecture you often have multiple APIs offering different services for the same user, accessible through a single frontend, like a SPA, a mobile app, a desktop solution, etc…
In this way, you can have for example:

  • an API for Authentication and user profile management.
  • multiple API for each service.
  • a frontend that talks to each API (or even multiple frontend).

In this article we’ll build two simple APIs powered by Laravel and JWT.

How will it works?

JWT stands for JSON Web Token, if you don’t know what it is: it’s an open standard to transmit information in JSON via signed tokens, you can read more about the standard here.

JWT Logo, Source: https://jwt.io/
JWT Logo, Source: https://jwt.io/
JWT Logo, Source: https://jwt.io/

JWT will handle user data for authentication (internal ID, email, permissions, IP, everything you consider important) signing them via an asymmetric algorithm like RS256 (RSA signature with SHA-256) that we’ll use to check user authentication between all our services.

We’ll use the Laravel framework to serve APIs that will generate tokens and serve different features from various endpoints where users are authenticated via the same JWT, handle by tymondesigns/jwt-auth package.

Projects setup

In first place, we want to create two laravel projects, I will use composer:

> composer create-project --prefer-dist laravel/laravel api-authentication
> composer create-project --prefer-dist laravel/laravel api-service1

We’ll have two folders with two fresh laravel projects. We’ll get in each folder and install the jwt package this way:

> composer require tymon/jwt-auth

This will change the composer.json by adding the jwt-auth package, while I’m writing this, I’m using the 1.1 version of the package. After package installation, we want to publish the package default configuration this way:

> php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

This will create a file config/jwt.php holding the JWT configuration we’ll tweak later.

For just building API, you may comment/remove the web routes in boot app/Providers/RouteServiceProvider.php:

public function boot() {
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->group(base_path('routes/api.php'));
/*
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
*/
});
}

We also want to create a folder for the keys we’ll use for signing (private and public key) in storage folder (or where you prefer) storage/jwt. For security purposes, We will create a .gitignore file inside of it that will help us not pushing the private and public key inside of our repository, this will just contain a line:*.pem.

Key generation

We’ll generate the keys just once, in our api-authentication and we’ll copy the keys into the other APIs projects.

For the pair of public and private key, we need to have a strong passphrase that we won’t share. In this example I’ll use this (the key that follows is just for this demo purpose, please change it for your system):

sO9sH6qT8jA0wV5gE5eT3kY2

The command to generate an RSA private key of 4096 bits encrypted via AES256 into storage/jwt/private.pem is:

openssl genrsa -passout pass:sO9sH6qT8jA0wV5gE5eT3kY2 -out storage/jwt/private.pem -aes256 4096

To generate the public key from our private key:

openssl rsa -passin pass:sO9sH6qT8jA0wV5gE5eT3kY2 -pubout -in storage/jwt/private.pem -out storage/jwt/public.pem

Now we have public.pem and private.pem into our storage/jwt folder.

JWT Config

Now we’ll add to the .env file our JWT configuration about the algorithm used and our keys:

JWT_ALGO=RS256
JWT_PUBLIC_KEY=jwt/public.pem
JWT_PRIVATE_KEY=jwt/private.pem
JWT_PASSPHRASE=sO9sH6qT8jA0wV5gE5eT3kY2

And we’ll fix the config/jwt.php file by letting Laravel load the private and public key from the storage:

'public' => 'file://'.storage_path(env('JWT_PUBLIC_KEY')),
'private' => 'file://'.storage_path(env('JWT_PRIVATE_KEY')),

Building authentication API

This section will just cover the api-authentication project.

Building this kind of authentication doesn’t differ from the default tymonjwt documentation, but we’ll build authentication anyway, in order to have a view on ever project step.

First you need your authenticatable model to implement JWTSubject (the entire code won’t be reported here, just the most considerable parts):

<?php 
namespace App\Models;

use
Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
public function getJWTIdentifier()
{
return $this->id;
}

public function getJWTCustomClaims()
{
return [
// Here you will put claims for your JWT: ip, device, permissions
];
}

}

After this, we want JWT to be our main authentication Guard by changing config/auth.php:

'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],

],

Then, we’ll build a new controller php artisan make:controller AuthController with a login method:

public function login(Request $request)
{
$credentials = $request->only('email', 'password');

if (!$token = auth()->attempt($credentials)) {
abort(406);
}

return response()->json([
'success' => true,
'data' => [
'token' => $token,
'token_type' => 'bearer',
]
]);
}

Last we add the binding to the route in routes/api.php:

Route::post('/login', [\App\Http\Controllers\AuthController::class, 'login']);

Does this already work? Test.

We already have user migration, model and factory on a standard laravel build, tweak them for your purpose and build a new test php artisan make:test AuthTest, open it and make a testLogin:

public function testLogin()
{
$user = User::factory()->createOne();
$response = $this->post(
'/api/v1/login',
[
'email' => $user->email,
'password' => 'password',
]
);
$response->assertStatus(200);
$response->assertJsonStructure([
'success',
'data' => [
'token',
'token_type',
]
]);
\JWTAuth::setToken($response->json('data.token'))->checkOrFail();
}

Now you can safely run php artisan test and if everything went well, you’ll get a result like this:

PASS  Tests\Feature\AuthTest
login
Tests: 1 passed
Time: 0.39s

Building other APIs authentication

Now that authentication is ready, we can move on the api-service1 project.

Ideally, this other API service won’t have access to the first API database, so we’ll build a no database authentication through laravel model, you can eventually edit in order to resolve the user to your preferred model.
Why this choice? Using multiple api endpoints for different services/features generally means that your services are completely separated between them and you don’t want pairing problems due to data replication, so referring to a user with just his minimal data not stored in a DB will help your system staying coherent.

To make our API works, we’ll clone the configuration from the config phase, by copying our keys and editing the config/jwt.php. We’ll also put our variables in the .env file.

In order to override default authentication, we’ll build a custom Guard step-by-step, make a new class app/Guard/JWTGuard.php:

<?php
namespace App\Guard;
use App\Models\User;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
use Tymon\JWTAuth\JWT;
class JWTGuard implements Guard
{
use GuardHelpers;
/**
* @var JWT $jwt
*/
protected JWT $jwt;
/**
* @var Request $request
*/
protected Request $request;
/**
* JWTGuard constructor.
* @param JWT $jwt
* @param Request $request
*/
public function __construct(JWT $jwt, Request $request) {
$this->jwt = $jwt;
$this->request = $request;
}
public function user() {
if (! is_null($this->user)) {
return $this->user;
}
if ($this->jwt->setRequest($this->request)->getToken() && $this->jwt->check()) {
$id = $this->jwt->payload()->get('sub');
$this->user = new User();
$this->user->id = $id;
// Set data from custom claims
return $this->user;
}
return null;
}
public function validate(array $credentials = []) { }
}

What does this mean? This is a custom Guard implementation: Guard needs different methods to be implemented, by using GuardHelpers we’ll skip that methods and focus on the logic that we need.
The methods still to be implemented are: user() and validate(). We don’t need validate() so we’ll keep it empty, and focus on user().

Our authentication token will come from the request and will be parsed via the JWT instance in properties that will be injected. If token validation passes, we’ll just make an instance of the User model, set the id (and the other data that we put in custom claims) got from the payload via array access and just return it without doing any database operation.

We also need to make some changes to our model:

<?php
namespace App\Models;

use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Model;
class User extends Model implements AuthenticatableContract
{
public function getAuthIdentifierName()
{
return 'id';
}
public function getAuthIdentifier()
{
return $this->id;
}
public function getAuthPassword()
{
return null;
}
public function getRememberToken()
{
return null;
}
public function setRememberToken($value) {}
public function getRememberTokenName() {}

}

We’ll remove our default Authenticatable implementation to implement the Authenticatable contract and its methods (getAuthIdentifierName, getAuthIdentifier, getAuthPassword, getRememberToken, setRememberToken, getRememberTokenName).

Now we have to tell Laravel that our guard exists into the boot method of the AuthServiceProvider:

public function boot()
{
$this->registerPolicies();
$this->app['auth']->extend(
'jwt-auth',
function ($app, $name, array $config) {
$guard = new JWTGuard(
$app['tymon.jwt'],
$app['request']
);
$app->refresh('request', $guard, 'setRequest'); return $guard;
}
);

}

For testing set up a basic route, like the one ready on the api.php:

Route::middleware('auth:jwt')->get('/user', function () {
return Auth::user();
});

Will the service API know who I am?

To test how this work, we must first issue a token from the auth API, mine was this:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJpYXQiOjE2MDY0MDU1MDMsImV4cCI6MTYwNjQwOTEwMywibmJmIjoxNjA2NDA1NTAzLCJqdGkiOiJjUlI5Q1BmbWtxME9sWHN6Iiwic3ViIjoxLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.B_RrqoCtN6k1vWUFgtAQjb4sTSZUDOhClwMXxHFfvtI8WFOJAbPCc2k_jPz1OMTJRsirko9X_fmQS2JSy0_pURNt45Ezn-JsJgN85rWKpK3x5VbFXudf3Ngh1L8kz1kAK928PIbuTVQqCmO9edDVN8LvQ9-klf5NN4JUpZcjbO3Qoobqko0Yuc6khUP-tbIASijVgaO8E2ehOdCppga4wldbHACLmks2Xe2YYN-lIdljvT3m2hKxAvX2LnT7NilVM7sstJydvhTk507-LhMfO8q71RUAF9pjTZ5gXDdUCGhw5VJjT7aUGNjMe96anuLA6fr1PtAtLlu2Jv6Qx2ijJNxWVV9wLo6ovnT2e9bl56f_rqXLMz1qFq_3xYA2ziQlpQtoPSa7HYxpqaxhFnw5ji6-iqQmwgkvDBISi0zXE9Z48X-7LvUO4Y341iFBKqpFgA5agDgmo-Y0hyg7aCUt9nvzSOyz77afKgaF5AedKEIE0fgrgFPkZcni5gUw1OZRfCEMhzDZj_zKOrMCzQZhTMYTtyz7xzqwTdV8JKk2GJ5qH27JlNhm0uA2TEGBB-KYvABRO7OL2fOARCCMo6LGC_SRoZB2LnbBbT6ZXGavjbegFaPAYdGowWwKhccTDuJQoeOWkIAQ1P4b6CS_FBxvQ7IXlUix9f164G0Yiavab_U

Next, spin up the laravel development test php artisan serve and perform a GET request to /api/user?token={TOKEN_HERE} or you can perform a GET request to /api/user with the HTTP Header Authorization: Bearer {TOKEN_HERE}.

If everything went well, your response should be:

{
"id": 1
}

And so it is! Your user is authenticated in the service API.

How can I manually verify the JWT signature?

Well, take the token issued before:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJpYXQiOjE2MDY0MDU1MDMsImV4cCI6MTYwNjQwOTEwMywibmJmIjoxNjA2NDA1NTAzLCJqdGkiOiJjUlI5Q1BmbWtxME9sWHN6Iiwic3ViIjoxLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.B_RrqoCtN6k1vWUFgtAQjb4sTSZUDOhClwMXxHFfvtI8WFOJAbPCc2k_jPz1OMTJRsirko9X_fmQS2JSy0_pURNt45Ezn-JsJgN85rWKpK3x5VbFXudf3Ngh1L8kz1kAK928PIbuTVQqCmO9edDVN8LvQ9-klf5NN4JUpZcjbO3Qoobqko0Yuc6khUP-tbIASijVgaO8E2ehOdCppga4wldbHACLmks2Xe2YYN-lIdljvT3m2hKxAvX2LnT7NilVM7sstJydvhTk507-LhMfO8q71RUAF9pjTZ5gXDdUCGhw5VJjT7aUGNjMe96anuLA6fr1PtAtLlu2Jv6Qx2ijJNxWVV9wLo6ovnT2e9bl56f_rqXLMz1qFq_3xYA2ziQlpQtoPSa7HYxpqaxhFnw5ji6-iqQmwgkvDBISi0zXE9Z48X-7LvUO4Y341iFBKqpFgA5agDgmo-Y0hyg7aCUt9nvzSOyz77afKgaF5AedKEIE0fgrgFPkZcni5gUw1OZRfCEMhzDZj_zKOrMCzQZhTMYTtyz7xzqwTdV8JKk2GJ5qH27JlNhm0uA2TEGBB-KYvABRO7OL2fOARCCMo6LGC_SRoZB2LnbBbT6ZXGavjbegFaPAYdGowWwKhccTDuJQoeOWkIAQ1P4b6CS_FBxvQ7IXlUix9f164G0Yiavab_U

If you open jwt.io, you can use the built-in client-side debugger to test your token validity and content. For this token the content is:

Image for post
Image for post
JWT Token Debugging: Content

You can easily read the sub field content indicating our user ID, but you can’t tell if token is valid that way. To check validity you have to paste your public key inside the “Verify Signature” field. My public key was:

-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7F7BG64826DJ6COaE41D
9oK6nSm33RZeovt4AzbGhYjyezl51rqPqm09p9F7UU5UMbx9JCLsjA835CYOU77L
jsMBkus+B88vi4Z+szCHqGQXqD6FRBNhid9Si7uY3cydnJEnboEAP/RgLTUrNjE/
4L4oq/Sev0WJ+oQyTOAX+z9QuUbwblWlecdQnSMQNjRWkjTHOjYVrChcKP4hR0O7
ZsGGzO6Cdcst4g3tywKIK2fuQetzXhecvrC75AOJsBwAga086RNmSFW076CzeUIx
8d+KvsMLUZKKCmT6QrC2J5DJNwT2JJUcMfryB4DuOZ+3VUsS8jgdBiYZvoBEsX/Y
pJ/vb0h9jFbcWDER4VcFZfXolyOO3i1JPGK8k8QiwGAoGpxjWipXmvXZLAPAahIe
baOCW7cYrLxC2/ICP8n9pVueeyUXh+geiU7bKDH53Cy5s2rVW/fah3cZQo5D0Oym
bbO0MmqP4QA81VX/Cfl2FPclIv9wRP78kCiy6YFk1wao4P0mtGQsVSWkgWi78/KS
goWgXOdYZ2r9zUEvc+6BVAKwYc2iyTt/zuffkypiyIoDLqSoQz1/JZ8PpX4wXSWE
VKXk8/3oBknl9aCpjCYD4VSZPIGMSB5rMaCKlUvdpg4e2lwiXWmbfJb/c4WOu5v8
k7rer4iBpbCqoROuQWDg+b8CAwEAAQ==
-----END PUBLIC KEY-----

So if you paste this into that field you will get a concrete verification:

Image for post
Image for post
JWT Token Debugging: Token Verified

The Startup

Medium's largest active publication, followed by +752K people. Follow to join our community.

Kalizi <Andrea>

Written by

IT Engineering PhD and still student 🎓 Mobile&Backend Dev 💻 Growth hacking Enthusiast ☕ Startupper 🚀 Metalhead 🤘🏻 With love, from Palermo ❤️

The Startup

Medium's largest active publication, followed by +752K people. Follow to join our community.

Kalizi <Andrea>

Written by

IT Engineering PhD and still student 🎓 Mobile&Backend Dev 💻 Growth hacking Enthusiast ☕ Startupper 🚀 Metalhead 🤘🏻 With love, from Palermo ❤️

The Startup

Medium's largest active publication, followed by +752K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store