Build JWT Authentication Between Multiple API With Laravel
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 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
✓ loginTests: 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;
}
);
}
Once Guard is defined and authentication is extended, we need to register it as our authentication guard in our config/auth.php
:
<?php
return [
'defaults' => [
'guard' => 'jwt',
'passwords' => 'users',
],
'guards' => [
// ...
'jwt' => [
'driver' => 'jwt-auth',
'provider' => 'users'
],
],
// ...
];
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:
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: