Skip to content
All posts
LaravelAPIDevOpsSecurity

Restrict JWT Token with IP Address using Laravel

September 28, 2021·Read on Medium·
Source: freepik

Authentication is one of the most important parts of any web application. Handling authentication between mobile application and server side can be tricky and demand more secure approach. One of the best approach is using JSON Web Token (JWT).

The structure of a JWT token consists of header, payload, and signature. Basically, server side just create the token with existing claims and the Front-end like VueJS use as authorization of any API request. Server side just validate whether valid token from issuer or not. Enough talk, here the question :-

Are JWT tokens secure?

By default JWT are not encrypted and the token is simply a base64 encoded that can be easily decoded to see the plain JSON content that the token carries.

So the response to the question is ‘It depends’. JWT depends heavily on a good configuration when issuing the tokens and in a correct use and proper validation of the consumed tokens.

For best practice, you may refer here. The best practice covers on how you can secure JWT. But, none of it mention on how to restrict the JWT token based on IP Address, or etc…

What is the solution then?

Here come the solution. I proposed where there are 3 way what we can use on restricting JWT origin based on below criteria

  • IP Address (Client IP)
  • User Agent (Client user agent)
  • Hostname (Server name)

All of the criteria will be used in the custom claim. So, the proposed solution are

  1. Generate JWT token with hash custom claim with one or all the origin of token’s ip address, user agent and hostname
  2. Create a Laravel middleware and validate payload of the token before the authentication happen

Lets get Started

An example payload would be like this

{
"iss":"example.com/api/token",
"iat": 1632451588,
"exp": 1632537988,
"nbf": 1632451588,
"jti": "R4MOzdBP8Hjv54fg",
"sub": 3170722,
}

From the payload, we will add another claims like below

{
"iss":"example.com/api/token",
"iat": 1632451588,
"exp": 1632537988,
"nbf": 1632451588,
"jti": "R4MOzdBP8Hjv54fg",
"sub": 3170722,
"hst": "XXXXXXX",
"ura": "XXXXXXX",
"ipa": "XXXXXXXX"
}

where hst referring to hostname, ura referring to User agent and ipa referring to IP Address. “hst”, “ura”, “ipa” is manmade parameters. You can freely use any wording to can other than reserved JWT claims.

So lets start with installing composer package (if not installed yet)

composer require tymon/jwt-auth

Based on quick start (i assumed you already use this package), implement JWTSubject to any model you want to use to make authentication. So, lets say table user

jwtsampleuser.php View on GitHub
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use HasFactory, Notifiable, HasProfilePhoto;

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

You will notice that there is method getJWTCustomClaims. This is where we can put our custom claims. Let’s put those 3 origin identity as we discuss earlier.

return [
'hst' => md5(gethostname()),
'ipa' => md5(request()->ip()),
'ura' => md5(request()->userAgent()),
];

i would like to suggest you hash the value. Because If you don’t, someone can easily decrypt the payload using online JWT debugger.

Now, generate the token

$token = auth()->attempt($request->only('username', 'password'))

It will generate like below

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL2FwaS1kZXYua2l0YWphZ2EuY28vYXBpL3BvcnRhbC90b2tlbiIsImlhdCI6MTYzMjY5ODUyMiwiZXhwIjoxNjMyNzg0OTIyLCJuYmYiOjE2MzI2OTg1MjIsImp0aSI6IkpYOGJESlNCelhoajh4bXYiLCJzdWIiOjEsInBydiI6ImMwOTczZDkxMDk1ZTI0MWE3YTNjNWRmZmI1ZWQ0ZDJlODdiOGQ2NWEiLCJoc2giOiIxZGFXcFlRRGJNR25WbU80UmRhNnI4Mjl6akxkYkt6UFFtazN2N0owNWd3Wk5xRWxvQTFXWGV4UEtKcFAiLCJkZWkiOiJhYWFhYi0xMjMyMy0zc2RhZHNhdjEyMzIzMS0yMjItMzMtYWRyaS15ZnlmMTIxIiwicHRuIjpudWxsLCJoc3QiOiJ............

Ok lets use JWT debugger to see the payload whether the custom claim is attached or not

You can see the last 3 claims in the Payload is attached successfully. Now all we need is the validation middleware.

php artisan make:middleware AuthenticateJWT

The logic is where, we fetch the JWT payload from each HTTP request and compare the values. If either one is not meet the requirement, simply throw the exception. Here is the full snippet :-

AuthenticateJWT.php View on GitHub
<?php

namespace App\Http\Middleware\Core;

use App\Helpers\Utils;
use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthenticateJWT
{
    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure $next
     * @return mixed
     * @throws JWTException
     */
    public function handle($request, Closure $next)
    {
        // Validating Payload
        if ($payload = JWTAuth::parseToken()->getPayload()) {
            if ($payload->get('ipa') != md5($request->ip())) {
                throw new JWTException(__('api.blocked_device_error', ['reason' => 'origin IP is invalid']), 403);
            }

            if ($payload->get('ura') != md5($request->userAgent())) {
                throw new JWTException(__('api.blocked_device_error', ['reason' => 'origin user agent is invalid']), 403);

            }
          
            if ($payload->get('hst') != md5(gethostname())) {
                throw new JWTException(__('api.blocked_device_error', ['reason' => 'origin hostname is invalid']), 403);

            }
        } else {
            throw new JWTException(__('api.token_error'), 403);
        }
        
        
        // Authenticate 
        if (! $user = JWTAuth::parseToken()->authenticate()) {
            throw new JWTException(__('api.token_error'), 403);
        }

        return $next($request);
    }
}

Explanation

Here is some explanation. What is above middleware do? It basically just validating the token creation origin. Here some situation,

Customer create a token by IP 10.10.10.10 with User Agent Safari Browser. When transmitting in network, attackers able to grab your JWT token. The attackers would try to authenticate using your JWT token on their side. Unfortunately, the token is rejected due to attacker IP (11.11.11.11) is different from the victim IP and same goes to the user agent. The token only can use by IP 10.10.10.10, else rejected by the server.

For the hostname restriction, is less recommended because if you have multiple server under load balancer, you need to make sure all of your server have same hostname. Else it wont work.

For hashing. I would like to recommend to hash the custom claims value with salts. Because, the basic MD5 would be easily to be brute.

Conclusions

There are many way if we want to secure anything in application. Of course, there is no system is safe. If we can’t focus on how to completely secure it, just think how to slow down the attackers mind.

That’s it. Hope its help 😁. Thanks for your time.

P/S: This article are not really meant for PHP developers. It’s also meant for other developer because the flow is just the same. The usage of JWT is completely the same in other programming language

References

https://www.bbva.com/en/json-web-tokens-jwt-how-to-use-them-safely/

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium