Saturday, March 18, 2017

Angular / Laravel JWT Auth, Storing token on cookie suggestions

I'm working on an AngularJS app with a Laravel 5 Backend and I'm using JWT Authentication, but instead of using the common approach of storing the token on LocalStorage on the client side, I decided to store the token on a cookie. I haven't found much information on this topic because most of the examples you can find out there are using LocalStorage. I found this post, that provided an approach which is kind of what I was looking for. I made some modifications, and this is what I have. On Laravel I'm using a middleware to check if requests to protected routes are valid

public function handle($request, Closure $next)
{
    try {
        if(!$request->headers->has('csrf-token')) 
            throw new TokenMismatchException();
        if (!$request->cookie('token'))
            throw new TokenMismatchException();
        $rawToken = $request->cookie('token');
        $token = new Token($rawToken);
        $payload = JWTAuth::decode($token);
        if($payload['csrf-token'] != $request->headers->get('csrf-token')) 
            throw new TokenMismatchException();
        Auth::loginUsingId($payload['sub']);
    } catch(\Exception $e) {
        if( $e instanceof TokenExpiredException) {
            $rawToken = $request->cookie('token');
            $token = new Token($rawToken);
            $newToken = new Token(JWTAuth::refresh($token));
            $newPayload = JWTAuth::decode($newToken);
            return response()->json($newPayload->toArray(), 498)
                   ->withCookie('token', $newToken, config('jwt.ttl'), "/", null, false, true);
        }
        if( $e instanceof TokenMismatchException) {
            return response()->json(['error' => 'token_mismatch'], 401);
        }
        //return response()->json(['error' => $e->getMessage()], 401);
        return response()->json(['error' => 'Unauthorized'], 401);
    }
    return $next($request);
}

Handling the login request:

protected function postLogin(Request $request) {
    $credentials = $request->only(['email', 'password']);
    try
    {
        $user = User::where('email','=',$credentials['email'])->first();

        if ( !($user && Hash::check($credentials['password'], $user->password) ))
        {
            return response()->json(['error' => 'invalid_credentials'], 401);
        }
        $customClaims = ['sub' => $user->id, 'csrf-token' => str_random(32) ];
        $payload = JWTFactory::make($customClaims);
        $token = JWTAuth::encode($payload);
    } catch(\Exception $e) {
        // TODO
    }
    return response()->json($payload->toArray())
           ->withCookie('token', $token, config('jwt.ttl'), "/", null, false, true);
}

And on the Angular side I'm using an $http interceptor to recover the request when the token expires.

var APIInterceptor = ['$rootScope', '$injector', function ($rootScope, $injector) {
        return {
            responseError : function (response) {
                var authService = $injector.get('authService');
                var $http = $injector.get('$http');
                var $state = $injector.get('$state');
                if (response.status === 498) {
                    authService.setSessionInfo(response.data);
                    response.config.headers = authService.getHeaders();
                    return $http(response.config);
                }
                if (response.status === 401) {
                    authService.logOut();
                    $state.go('login');
                }
            }
        }
    }];

So in this approach what is sent on every request in the headers is a csrf-token which is validated against the csrf-token on the payload of the JWT token that is sent with the cookie. This is working for me just fine, but this is the first time I'm using JWT by storing it in a cookie instead of sending the token on the "Authorization: Bearer" on the headers on every request, so I would like hear any suggestions on whether this approach is secure (which is ultimately the whole reason why I'm doing it), or any considerations/improvements I could make to it. Also If you notice I'm using a status 498 for when the token needs to be refreshed, I've read that the standard is 401 but since the http interceptor on angular is also listening for a 401 to redirect to the login when the request is unauthorized, I'd like to know which status code could be used instead to be more compliant of the standards.



via hrivera

Advertisement