Laravel JWT multi table validation isolation

Time:2019-11-8

Why quarantine

When the same laravel project has multiple terminals (mobile terminal, management terminal, etc.) that need to use JWT for user authentication, if there are multiple user tables (generally, there will be), it needs to do token isolation, otherwise, there will be a problem that the token of the mobile terminal can also request the management terminal, causing the user to exceed the authority.

The reason for this problem is that by default, the JWT token of laravel only stores the value of the primary key of the data table, and there is no distinction between the values of that table. So as long as the ID carried in the token exists in your user table, it will lead to unauthorized authentication.

Let’s take a look at the original appearance of laravel’s JWT token:

1

2

3

4

5

6

7

8

9

{

    "iss": "http://your-request-url",

    "iat": 1558668215,

    "exp": 1645068215,

    "nbf": 1558668215,

    "jti": "XakIDuG7K0jeWGDi",

    "sub": 1,

    "prv": "92d5e8eb1b38ccd11476896c19b0e44512b2aacd"

}

The sub field carries the data, and the other fields are JWT’s validation fields.

We only see that the value of sub is 1, which does not indicate which table or verifier it is. When the token passes your authentication middleware, you can get users with corresponding table ID 1 by using different guards (for guard, please check the laravel document).

Terms of settlement

To solve the problem of user exceeding authority, we just need to bring our custom field on the token to distinguish which table or which verifier generated it, and then write our own middleware to verify whether our custom field meets our expectations.

Add custom information to token

We know that to use JWT authentication, the user model must implement the interface of jwtsubject (the code is taken from the JWT document):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

<?php

 

namespace App;

 

use Tymon\JWTAuth\Contracts\JWTSubject;

use Illuminate\Notifications\Notifiable;

use Illuminate\Foundation\Auth\User as Authenticatable;

 

class User extends Authenticatable implements JWTSubject

{

    use Notifiable;

 

    // 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 [];

    }

}

Let’s look at the functions of the two methods implemented:

  • Getjwtidentifier: to get the identity that will be stored in the JWT declaration, we need to return the name of the primary key field identifying the user table. Here is the primary key ‘ID’,
  • Getjwtcustomclaims: returns the array containing the custom key value pairs to be added to the JWT declaration, which returns an empty array without adding any custom information.

Next we can add our custom information to the user model that implements the getjwtcustomclaims method.

Administrator model:

1

2

3

4

5

6

7

8

9

/**

 *Additional customization added to JWT load

 *

 * @return array

 */

public function getJWTCustomClaims()

{

    return ['role' => 'admin'];

}

Mobile user model:

1

2

3

4

5

6

7

8

9

/**

 *Additional customization added to JWT load

 *

 * @return array

 */

public function getJWTCustomClaims()

{

    return ['role' => 'user'];

}

A role name is added as the user ID.

In this way, the token generated by the administrator will look like this:

1

2

3

4

5

6

7

8

9

10

{

    "iss": "http://your-request-url",

    "iat": 1558668215,

    "exp": 1645068215,

    "nbf": 1558668215,

    "jti": "XakIDuG7K0jeWGDi",

    "sub": 1,

    "prv": "92d5e8eb1b38ccd11476896c19b0e44512b2aacd",

    "role": "admin"

}

The token generated by the mobile end user will look like this:

1

2

3

4

5

6

7

8

9

10

{

    "iss": "http://your-request-url",

    "iat": 1558668215,

    "exp": 1645068215,

    "nbf": 1558668215,

    "jti": "XakIDuG7K0jeWGDi",

    "sub": 1,

    "prv": "92d5e8eb1b38ccd11476896c19b0e44512b2aacd",

    "role": "user"

}

We can see that there is an additional role field added by ourselves, which corresponds to our user model.

Next, we write a middleware by ourselves. After analyzing the token, we can determine whether it is the role we want. If it is a corresponding role, it will pass. If it is not a corresponding role, it will be reported as 401.

Write JWT role verification Middleware

A middleware that can be used globally is provided here (recommended before user authentication Middleware):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

<?php

/**

 * Created by PhpStorm.

 * User: wlalala

 * Date: 2019-04-17

 * Time: 13:55

 */

 

namespace App\Http\Middleware;

 

use Closure;

use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

use Tymon\JWTAuth\Exceptions\JWTException;

use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;

 

class JWTRoleAuth extends BaseMiddleware

{

    /**

     * Handle an incoming request.

     *

     * @param $request

     * @param Closure $next

     * @param null $role

     * @return mixed

     */

    public function handle($request, Closure $next, $role = null)

    {

        try {

            //Analyze token role

            $token_role = $this->auth->parseToken()->getClaim('role');

        } catch (JWTException $e) {

            /**

             *Token resolution failed, indicating that there is no token available in the request.

             *In order to be used globally (requests that do not need token can also pass), let the request continue here.

             *Because the responsibility of this middleware is only to verify the role of token.

             */

            return $next($request);

        }

 

        //Determine the token role.

        if ($token_role != $role) {

            throw new UnauthorizedHttpException('jwt-auth', 'User role error');

        }

 

        return $next($request);

    }

}

Register JWT role verification Middleware

Register Middleware in APP / HTTP / kernel.php:

1

2

3

4

5

6

7

8

9

10

11

12

13

/**

 * The application's route middleware.

 *

 * These middleware may be assigned to groups or used individually.

 *

 * @var array

 */

protected $routeMiddleware = [

    //... omit

 

    //Multi table JWT verification

    'jwt.role' => \App\Http\Middleware\JWTRoleAuth::class,

];

Using JWT role verification Middleware

Next, add our middleware to the routing group that needs user authentication:

1

2

3

4

5

6

7

8

9

10

11

12

13

Route::group([

    'middleware' => ['jwt.role:admin', 'jwt.auth'],

], function ($router) {

    //Administrator verifies route

    // ...

});

 

Route::group([

    'middleware' => ['jwt.role:user', 'jwt.auth'],

], function ($router) {

    //Mobile end user authentication route

    // ...

});

At this point, JWT multi table user authentication isolation is completed.