Jwt-auth: Lumen exception handling in 1.0.0-alpha1

Created on 15 Mar 2016  路  6Comments  路  Source: tymondesigns/jwt-auth

I noticed that one change in the alpha version is errors now throw a Symfony-type exception rather than returning a json response.

Example: https://github.com/tymondesigns/jwt-auth/blob/develop/src/Http/Middleware/BaseMiddleware.php#L45

If I'm using this with Lumen, do I need to add global exception handlers for all error classes? If so, do you have an example handy that I can copy?

Most helpful comment

The previous behavior was dropped because it turned out there were many cases where people didn't want responses and using exceptions was a more general solution. Laravel 5+'s Handler also supports exception-based control flow a lot better (and encourages it some extent), so paralleling that is a bit more natural.

You can use global capture if you want. Here's an example I'm using. Inside Handler->render:

if ($e instanceof \Symfony\Component\HttpKernel\Exception\BadRequestHttpException) {
    return $this->TokenMissing();
}
if ($e instanceof \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException) {
    // TODO: Maybe writing our own middleware for this would be better
    // than trying to distinguish between them based only upon strings.
    if ($e->getMessage() === 'Token has expired') {
        // This case must be distinguished because our front-end runs a 
        // "refresh-as-necessary" approach using expiration as a trigger
        return $this->TokenExpired();
    } else {
        return $this->TokenInvalid();
    }
}

Then the convenience methods within Handler are all something like this:

private function TokenExpired()
{
    return response()->json([
        'error'   => 'TokenExpired',
        'path'    => $this->request->path(),
        'message' => $this->exception->getMessage(),
    ], HTTP_UNAUTHORIZED);
}

(Though you could of course do this inline and without the convenience properties $this->request and $this->exception.)

Of course, read my todo note in there... it's probably safer to check unique exception types (like the JWTException subclasses) in case someday something unexpected throws a Symfony exception and you assume it's a token thing. But changing the exceptions would require custom middleware, ad if you're doing that, you might just want it to return the responses directly and avoid the global handler altogether. Just swap out the exceptions in BaseMiddleware and its children with the responses you want and save them as new middlewares.

All 6 comments

Any update on this? I'm running into the same problem, instead of JSON we get the exception when specifying \Tymon\JWTAuth\Http\Middleware\Authenticate as route middleware (using before).

Any way to restore the previous behavior?

The previous behavior was dropped because it turned out there were many cases where people didn't want responses and using exceptions was a more general solution. Laravel 5+'s Handler also supports exception-based control flow a lot better (and encourages it some extent), so paralleling that is a bit more natural.

You can use global capture if you want. Here's an example I'm using. Inside Handler->render:

if ($e instanceof \Symfony\Component\HttpKernel\Exception\BadRequestHttpException) {
    return $this->TokenMissing();
}
if ($e instanceof \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException) {
    // TODO: Maybe writing our own middleware for this would be better
    // than trying to distinguish between them based only upon strings.
    if ($e->getMessage() === 'Token has expired') {
        // This case must be distinguished because our front-end runs a 
        // "refresh-as-necessary" approach using expiration as a trigger
        return $this->TokenExpired();
    } else {
        return $this->TokenInvalid();
    }
}

Then the convenience methods within Handler are all something like this:

private function TokenExpired()
{
    return response()->json([
        'error'   => 'TokenExpired',
        'path'    => $this->request->path(),
        'message' => $this->exception->getMessage(),
    ], HTTP_UNAUTHORIZED);
}

(Though you could of course do this inline and without the convenience properties $this->request and $this->exception.)

Of course, read my todo note in there... it's probably safer to check unique exception types (like the JWTException subclasses) in case someday something unexpected throws a Symfony exception and you assume it's a token thing. But changing the exceptions would require custom middleware, ad if you're doing that, you might just want it to return the responses directly and avoid the global handler altogether. Just swap out the exceptions in BaseMiddleware and its children with the responses you want and save them as new middlewares.

Thank you for the feedback. Do you suggest modifying BaseMiddleware of jwt-auth or deriving from it?

I would just use it as a template to copy from and make a brand new class that doesn't formally inherit or override it.

This works fine without much code duplication:

<?php
namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\Http\Middleware\Authenticate;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

/*
 * We override the standard class to return a JSON response instead
 * of a Symfony exception
 */
class JwtAuthenticate extends Authenticate
{

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        try {
            $this->authenticate($request);
            return $next($request);
        } catch(BadRequestHttpException $e) {
            return response()->json([
                'error'   => 'token_not_provided',
                'path'    => $request->path(),
                'message' => $e->getMessage(),
            ], 400);
        } catch (UnauthorizedHttpException $e) {
            return response()->json([
                'error'   => 'unauthorized',
                'path'    => $request->path(),
                'message' => $e->getMessage(),
            ], 401);
        }
    }
}

I had to patch the throttle middleware due to a Laravel/Lumen bug, see here: https://github.com/laravel/framework/issues/12309

Updated to also handle UnauthorizedHttpException

Looks good! I assume you know it can still throw an UnauthorizedHttpException when the token is invalid, expired, or blacklisted?

Was this page helpful?
0 / 5 - 0 ratings