← Back to blog

JWT with Laravel

| Laravel

This article was published over 2 years ago. Some information may be outdated.

A few weeks ago, I wanted to use JSON Web Token in one of my Laravel projects.

After some research, I found several composer packages. But I was thinking: why do I need a package for something this simple?

I am not a fan of pulling in composer packages for every single problem.

JWT provides official PHP support via the firebase/php-jwt composer library, which handles all the heavy lifting (encoding/decoding).

If you prefer using full-featured composer packages for JWT, this post is not for you. I only use firebase/php-jwt here.

I will be using Laravel 8 with an SQLite database. Create a new Laravel project:

laravel new jwt
cd jwt
composer require laravel/ui "^2.0"
php artisan ui bootstrap --auth && npm install && npm run dev

You are free to use whatever database driver suits your needs, but I will use SQLite.

Setting up the database

Create the SQLite database:

cd jwt/database
sqlite3 database.sqlite "create table t(id int); drop table t;"

SQLite does not allow creating empty databases, so I created a table and dropped it immediately to get an empty database.

Update your .env and set DATABASE_CONNECTION to sqlite.

You need some users for testing. Open database/seeds/DatabaseSeeder.php and add the following line:

factory(App\User::class, 10)->create();

Run the migrations and seeders:

php artisan migrate:fresh --seed

Authentication Guards in Laravel

A Laravel guard acts like a real guard sitting in front of the entrance door, checking people for a valid ID.

Since you are creating a new authentication mechanism, you need a new guard that inspects incoming requests and validates the JWT token.

Laravel provides two ways of creating guards. The easiest is using closures.

Open your AuthServiceProvider and add the following code inside the boot method:

 \Auth::viaRequest('email', function ($request) {
    return App\User::where('email', $request->email)->first();
 });

The guard must return a User model. If the user cannot be found, return null.

Open the config/auth.php file and add the new guard to the guards section:

'guards' =>
    'email' => [
            'driver' => 'email',
            'provider' => 'users'
    ]
]

Here is how to use it.

Open your routes/web.php file and add the following code:

Route::get('/dashboard', function() {
    return response('Hello '.\Auth::user()->name);
})->middleware('auth:email');

By using middleware('auth:email') you are telling Laravel to use the email guard.

Run Laravel's internal server:

php artisan serve

Try to access the dashboard route. You will be redirected to the login page because you did not provide a valid email address.

Try again, this time with a valid email address.

Get an email address from the database:

sqlite3 database/database.sqlite "select email from users limit 1"

Copy the returned email address, then access the dashboard route with that email:

http://127.0.0.1:8000/test?email=cmarquardt@example.org

You should see the content: Hello {user.name}.

The other way to add a new guard is using the Auth::extend method. With this approach, you return an object that implements the Illuminate\Contracts\Guard interface.

Take a look at the guard interface to see what methods need to be implemented.

What is JSON Web Token?

JSON Web Token (JWT) is a base64-encoded JSON object encrypted by one of the supported algorithms such as HS256, RS256, etc. Nobody can tamper with the data without having the decryption key.

While JWT can be used for anything, it is mostly used as an authentication/authorization mechanism.

Once the user logs in with her username and password, a new JSON Web Token is sent back to her. She then uses this token to perform protected actions such as updating her profile, inserting new data, and so on.

The token can have an expiration date.

You are probably wondering how Laravel knows that the received token belongs to a specific user.

Before answering that, here is what a JWT looks like.

JWT consists of three parts:

  • Header: specifies the encryption algorithm such as HS256, RS256, etc.
  • Payload: the actual data being exchanged, such as the user id, password, etc.
  • Signature: created by combining the header, payload, and the secret key.

Header: the simplest part. It specifies the algorithm such as HS256 and the type (which is always JWT):

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload: contains the claims. The claims describe the entity (typically, the user). Claims are divided into three parts, but only two are needed:

  • Registered Claims: predefined claims that are not mandatory but recommended, such as the expiration time (exp) and others.
  • Private claims: custom claims created to share information, such as the user id, user type, etc.
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature: created by taking the encoded header, the encoded payload, and signing them with the algorithm specified in the header (along with the secret key).

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

The sub claim is normally used to store the user id.

JWT Configurations

First, install the firebase/php-jwt package:

composer require firebase/php-jwt

You need private and public keys for encrypting/decrypting the payload.

I usually put these keys in the storage directory, but you can choose any appropriate directory path:

cd ~my-jwt-project/storage
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub

Create a new config file in config/jwt.php:

<?php

return [
    'private_key' => storage_path('jwtRS256.key'),

    'public_key' => storage_path('jwtRS256.key.pub'),

    'ttl' => 86400, // in seconds

    'leeway' => 60, // in seconds

    'encrypt_algo' => 'RS256',

    'allowed_algo' => ['RS256']
];

I use the RS256 algorithm here. If you want a different one, look at the supported algorithms.

For more about the leeway option, read up on the nbf claim. One minute is sufficient for clock skew.

I will create four classes:

  • JwtBuilder: responsible for creating the token using convenient methods.
  • JwtParser: reads and decrypts the given token.
  • JwtGuard: the Laravel guard that authenticates the user via the token.
  • JwtAuth: authenticates the user.

JwtBuilder

JwtBuilder is a wrapper around the claims. It provides readable methods -- for example, calling relatedTo is more descriptive than aud.

I have also added the claims descriptions for each method.

Create a new class app/Services/Jwt/JwtBuilder.php:

<?php

namespace App\Services\Auth;

use Carbon\CarbonInterface;
use Firebase\JWT\JWT;

class JwtBuilder
{
    protected $claims;

    /**
     *  The "iss" (issuer) claim identifies the principal that issued the
     *  JWT.  The processing of this claim is generally application specific.
     *  The "iss" value is a case-sensitive string containing a StringOrURI
     *  value.  Use of this claim is OPTIONAL.
     *
     * @param $val
     * @return $this
     */
    public function issuedBy($val): self
    {
        return $this->registerClaim('iss', $val);
    }

    /**
     * The iat (issued at) claim identifies the time at which the JWT was issued.
     * This claim can be used to determine the age of the JWT.
     * Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.
     *
     * @param $val
     * @return $this
     */
    public function issuedAt($val)
    {
        return $this->registerClaim('iat', $val);
    }

    /**
     *  The "sub" (subject) claim identifies the principal that is the
     *  subject of the JWT.  The claims in a JWT are normally statements
     *  about the subject.  The subject value MUST either be scoped to be
     *  locally unique in the context of the issuer or be globally unique.
     *  The processing of this claim is generally application specific.  The
     *  "*sub" value is a case-sensitive string containing a StringOrURI
     *  value.  Use of this claim is OPTIONAL.
     *
     * @param $val
     * @return $this
     */
    public function relatedTo($val)
    {
        return $this->registerClaim('sub', $val);
    }

    /**
     * The aud (audience) claim identifies the recipients that the JWT is intended for.
     * Each principal intended to process the JWT MUST identify itself with a value in the audience claim.
     * If the principal processing the claim does not identify itself with a value in the aud claim when this
     * claim is present, then the JWT MUST be rejected. In the general case, the aud value is an array
     * of case-sensitive strings, each containing a StringOrURI value.
     * In the special case when the JWT has one audience, the aud value MAY be a single case-sensitive string
     * containing a StringOrURI value. The interpretation of audience values is generally application specific.
     * Use of this claim is OPTIONAL.
     *
     * @param $name
     * @return $this
     */
    public function audience($name)
    {
        return $this->registerClaim('aud', $name);
    }

    /**
     * The exp (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted
     * for processing.
     * The processing of the exp claim requires that the current date/time MUST be before the expiration date/time
     * listed in the exp claim. Implementers MAY provide for some small leeway, usually no more than a few minutes,
     * to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.
     *
     * @param CarbonInterface $dateTime
     * @return $this
     */
    public function expiresAt(CarbonInterface $dateTime)
    {
        return $this->registerClaim('exp', $dateTime->timestamp);
    }

    /**
     * The jti (JWT ID) claim provides a unique identifier for the JWT.
     * The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the
     * same value will be accidentally assigned to a different data object; if the application uses multiple issuers,
     * collisions MUST be prevented among values produced by different issuers as well. The jti claim can be used to
     * prevent the JWT from being replayed. The jti value is a case-sensitive string. Use of this claim is OPTIONAL.
     *
     * @param $val
     * @return $this
     */
    public function identifiedBy($val)
    {
        return $this->registerClaim('jti', $val);
    }

    /**
     * The nbf (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.
     * The processing of the nbf claim requires that the current date/time MUST be after or equal to the not-before
     * date/time listed in the nbf claim. Implementers MAY provide for some small leeway, usually no more than a
     * few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value.
     * Use of this claim is OPTIONAL.
     *
     * @param CarbonInterface $carbon
     * @return $this
     */
    public function canOnlyBeUsedAfter(CarbonInterface $carbon)
    {
        return $this->registerClaim('nbf', $carbon->timestamp);
    }

    public function withClaim($name, $value)
    {
        return $this->registerClaim($name, $value);
    }

    public function withClaims(array $claims): self
    {
        foreach ($claims as $name => $value) {
            $this->withClaim($name, $value);
        }
        return $this;
    }

    public function getToken()
    {
        return JWT::encode($this->claims, $this->getPrivateKey(), $this->getAlgo());
    }

    protected function getPrivateKey(): string
    {
        return file_get_contents(config('jwt.private_key'));
    }

    protected function getAlgo()
    {
        return config('jwt.encrypt_algo');
    }

    protected function registerClaim(string $name, string $val): self
    {
        $this->claims[$name] = $val;
        return $this;
    }
}

JwtParser

The JwtParser takes a token, decodes it, and saves the result in the $claims property. You then access these claims through readable methods such as getRelatedTo.

Create a new class app/Services/Jwt/JwtParser.php:

<?php

namespace App\Services\Auth;

use Firebase\JWT\JWT;

class JwtParser
{
    /**
     * @var array|object
     */
    protected $claims;

    public function __construct(string $token)
    {
        JWT::$leeway = $this->getLeeway();
        $this->claims = JWT::decode($token, $this->getPublicKey(), $this->supportedAlgos());
    }

    public static function loadFromToken(string $token)
    {
        return new self($token);
    }

    public function getIssuedBy()
    {
        return $this->getClaim('iss');
    }

    public function getIssuedAt()
    {
        return $this->getClaim('iat');
    }

    public function getRelatedTo()
    {
        return $this->getClaim('sub');
    }

    public function getAudience()
    {
        return $this->getClaim('aud');
    }

    public function getExpiresAt()
    {
        return $this->getClaim('exp');
    }

    public function getIdentifiedBy()
    {
        return $this->getClaim('jti');
    }

    public function getCanOnlyBeUsedAfter()
    {
        return $this->getClaim('nbf');
    }

    protected function getClaim(string $name)
    {
        return $this->claims->{$name} ?? null;
    }

    protected function getPublicKey(): string
    {
        return file_get_contents(config('jwt.public_key'));
    }

    protected function getAlgo()
    {
        return config('jwt.encrypt_algo');
    }

    protected function getLeeway()
    {
        return config('jwt.leeway');
    }

    protected function supportedAlgos()
    {
        return config('jwt.supported_algos');
    }
}

JwtGuard

The guard contains the actual JWT authentication process. It reads the bearer token, decodes it, and loads the appropriate user by inspecting the sub claim.

The sub claim contains the user id. If you look at JwtAuth, you will see that the user id is stored in the sub (via the relatedTo method).

Since the token is encrypted with RSA256, only you have the ability to decode it with the private key. You can rely on the sub value with confidence.

<?php

namespace App\Services\Auth;

use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\{Guard, UserProvider};
use Illuminate\Http\Request;

class JwtGuard implements Guard
{
    use GuardHelpers;

    /**
     * @var Request
     */
    private $request;

    private $lastAttempted;

    public function __construct(UserProvider $provider, Request $request)
    {
        $this->provider = $provider;
        $this->request = $request;
    }

    public function user()
    {
        if (! is_null($this->user)) {
            return $this->user;
        }

        return $this->user = $this->authenticateByToken();
    }

    public function validate(array $credentials = [])
    {
        if (! $credentials) {
            return false;
        }

        if ($this->provider->retrieveByCredentials($credentials)) {
            return true;
        }

        return false;
    }

    protected function authenticateByToken()
    {
        if (! empty($this->user)) {
            return $this->user;
        }

        $token = $this->getBearerToken();

        if (empty($token)) {
            return null;
        }

        try {
            $decoded = $this->authenticatedAccessToken($token);

            if (! $decoded) {
                $user = null;
            } else {
                $user = $this->provider->retrieveById($decoded->getRelatedTo());
            }

        } catch (\Exception $exception) {
            logger($exception);
            $user = null;
        }

        return $user;
    }

    protected function getBearerToken()
    {
        return $this->request->bearerToken();
    }

    public function attempt(array $credentials = [], $login = true)
    {
        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

        if ($this->hasValidCredentials($user, $credentials)) {
            $this->user = $user;
            return true;
        }

        return false;
    }

    protected function hasValidCredentials($user, $credentials)
    {
        return $user !== null && $this->provider->validateCredentials($user, $credentials);
    }

    public function authenticatedAccessToken($token)
    {
        return JwtParser::loadFromToken($token);
    }
}

JwtAuth

This class generates the JWT upon successful authentication. The authenticateAndReturnJwtToken method must be called from a controller.

Create a new class app/Services/Jwt/JwtAuth.php:

<?php

namespace App\Services\Jwt;

use App\Services\Auth\JwtBuilder;
use App\User;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\Auth;

class JwtAuth
{
    public function authenticateAndReturnJwtToken(string $email, string $password): ?string
    {
        if (! Auth::attempt(['email' => $email, 'password' => $password])) {
            return false;
        }

        try {
            /** @var User $user */
            return $this->createJwtToken(Auth::user());

        } catch (\Throwable $exception) {
            logger($exception->getMessage());
            return false;
        }
    }

    protected function createJwtToken(User $user, CarbonInterface $ttl = null): string
    {
        return (new JwtBuilder())
            ->issuedBy(config('app.url'))
            ->audience(config('app.name'))
            ->issuedAt(now())
            ->canOnlyBeUsedAfter(now()->addMinute())
            ->expiresAt($ttl ?? now()->addSeconds(config('jwt.ttl')))
            ->relatedTo($user->id)
            ->getToken();
    }
}

Putting it all together

Start by creating the AuthController.

The AuthController authenticates the user using the default guard (web). If authentication succeeds, a new token is sent back to the user.

php artisan make:controller AuthController
<?php

namespace App\Http\Controllers;

use App\Services\Jwt\JwtAuth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{
    public function __invoke(Request $request, JwtAuth $jwtAuth)
    {
        $valid = Validator::make($request->all(), [
            'email'     => 'required|email|exists:users,email',
            'password'  => 'required|string|min:8',
        ]);

        if ($errors = $valid->errors()->all()) {
            return response()->json(['errors' => $errors], 400);
        }

        if ($token = $jwtAuth->authenticateAndReturnJwtToken($request->email, $request->password)) {
            return response()->json(['token' => $token]);
        }

        return response()->json(['errors' => 'Cannot authenticated!'], 400);
    }
}

Add the authentication route in routes/api.php:

Route::post('auth', 'AuthController')->name('jwt.auth');

Test it:

curl --location --request POST 'http://127.0.0.1:8000/api/auth' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data-raw '{
    "email" : "myemail@example.com",
    "password" : "password"
}'

If authentication succeeds, you will see the token:

{
    "token": "{jwt token goes here}"
}

Now that you have the token, here is how to use it for authentication.

Open routes/api.php and add the following route:

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

Authenticate via the generated token:

curl --location --request GET 'http://127.0.0.1:8000/api/user' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {generate token goes here}'

If authentication succeeds, you will see the authenticated user information:

{
    "id": 1,
    "name": "Elissa Dickens",
    "email": "janice.harber@example.net",
    "email_verified_at": "2020-03-17T11:00:06.000000Z",
    "created_at": "2020-03-17T11:00:06.000000Z",
    "updated_at": "2020-03-17T11:00:06.000000Z"
}

Summary

  • JWT is a base64-encoded, encrypted JSON object -- it contains claims (header, payload, signature) that cannot be tampered with without the decryption key.
  • Laravel guards are the right extension point for custom authentication -- implement the Guard interface to inspect bearer tokens and resolve users from the sub claim.
  • firebase/php-jwt is all you need -- it handles encoding and decoding, so you do not need a full-featured JWT package.
  • The JwtBuilder and JwtParser classes wrap claim management -- they provide readable, self-documenting methods like relatedTo and expiresAt instead of raw claim keys.
  • Consider Sanctum for SPA/mobile apps -- if you have a SPA or mobile app, Sanctum is a first-party Laravel package built specifically for simple API authentication.
Share