user4287915
user4287915

Reputation: 366

Decode JWT and create authenticated User in Laravel microservice, merging with local user data

Background

I have a microservice setup the flow is:

client > api gateway > auth server > api gateway > microservice
  1. The client has a 'external' JWT from Laravel passport
  2. Client sends request to the api gateway with the 'external' JWT
  3. The api gateway sends a request to the auth server (Laravel passport) with the 'external' JWT
  4. The auth server verifies the user is still active and returns a new 'internal' JWT to the api gateway containing the users profile, groups etc
  5. The api gateway forwards the request with this new 'internal' JWT to the microservice
  6. (all fine up to this point)
  7. The Microservice verifies the 'internal' JWT using the auth servers public key
  8. The microservice decodes the 'internal' JWT and creates a user object from the profile contained within
  9. If the microservice has a local users table (e.g. for microservice specific user data), merge the local data with the JWT data

Microservice Authentication

I have created a JwtGuard that can decode the JWT and create a user using GenericUser:

auth.php

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],
],

AuthServiceProvider.php

public function boot()
    {
        $this->registerPolicies();

        Auth::extend('jwt', function ($app) {
            return new JwtGuard($app['request']);
        });
    }

JwtGuard.php

<?php
namespace App\Services\Auth;

use Illuminate\Auth\GenericUser;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;

use \Firebase\JWT\JWT;
use Illuminate\Http\Request;

class JwtGuard implements Guard {

    use GuardHelpers;

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

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

    /**
     * Get the currently authenticated user.
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user()
    {
        if (!is_null($this->user)) {
            return $this->user;
        }

        if(!$jwt = $this->getJwt()) {
            return null;
        }

        return $this->decode($jwt);
    }

    /**
     * Validate a user's credentials.
     *
     * @param  array $credentials
     * @return bool
     */
    public function validate(array $credentials = [])
    {
        if(!$jwt = $this->getJwt()) {
            return false;
        }

        return !is_null($this->decode($jwt))?true:false;
    }

    /**
     * Decode JWT and return user
     *
     * @return mixed|null
     */
    private function decode($jwt)
    {
        $publicKey = file_get_contents(storage_path('oauth-public.key'));

        try {
            $res = JWT::decode($jwt, $publicKey, array('RS256'));
            return $this->user = new GenericUser(json_decode(json_encode($res->user), true));
        } catch (\Exception $e) {
            return null;
        }
    }

    private function hasAuthHeader()
    {
        return $this->request->header('Authorization')?true:false;
    }

    private function getJwt()
    {
        if(!$this->hasAuthHeader()){
            return null;
        }

        preg_match('/Bearer\s((.*)\.(.*)\.(.*))/', $this->request->header('Authorization'), $jwt);

        return $jwt[1]?$jwt[1]:null;
    }

}

The problem

This works ok(ish), except that:


What I have so far

I have tried the following to merge the local user data with the JWT profile:

private function decode($jwt)
    {
        $publicKey = file_get_contents(storage_path('oauth-public.key'));

        try {
            $res = JWT::decode($jwt, $publicKey, array('RS256'));
            $this->user = new GenericUser(json_decode(json_encode($res->user), true));
            $this->user->localUser = \App\User::where('user_id', $this->user->id)->first();
            return $this->user;
        } catch (\Exception $e) {
            return null;
        }
    }

but this still leaves GenericUser not having the can() function.


Help...please!

I can't help feel there is a better (proper?) way to achieve this using 'User' instead of 'GenericUser' which will allow all the Authentication/Authorization features in Laravel to work properly, and to merge the data easily.

Upvotes: 1

Views: 6853

Answers (2)

Francisco Daniel
Francisco Daniel

Reputation: 1029

An easy way to achieve this is:

use Firebase\JWT\JWT;
use Laravel\Passport\Token;

$jwt = 'eyJ0...';
$publicKey = file_get_contents(storage_path('oauth-public.key'));
$res = JWT::decode($jwtToken, $publicKey, ['RS256']);
$user = Token::findOrFail($res->jti)->user;

Upvotes: 2

user4287915
user4287915

Reputation: 366

I solved it by adding $jwt_user to User construct to skip 'fillable':

auth.php

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

AuthServiceProvider.php

use App\User;
use \Firebase\JWT\JWT;

public function boot()
    {
        $this->registerPolicies();

        Auth::viaRequest('jwt', function ($request) {
            $publicKey = file_get_contents(storage_path('oauth-public.key'));

            if(!$hasAuthHeader = $request->header('Authorization')?true:false){
                return null;
            }

            preg_match('/Bearer\s((.*)\.(.*)\.(.*))/', $request->header('Authorization'), $jwt);

            try {
                $res                        = JWT::decode($jwt[1], $publicKey, array('RS256'));
                $jwt_user                   = json_decode(json_encode($res->user), true);
                $local_user                 = User::find($jwt_user['id']);
                $jwt_user['local_profile']  = $local_user?$local_user:[];
                $user                       = new User([], $jwt_user);
                return $user;
            } catch (\Exception $e) {
                return null;
            }
        });
    }

User.php

public function __construct(array $attributes = array(), $jwt_user = array())
    {
        parent::__construct($attributes);

        foreach($jwt_user as $k=>$v){
            $this->$k = $v;
        }
    }

Upvotes: 6

Related Questions