6

Its to my knowledge that a JWT based authorization system is usually reserved for SPA'S ( you know, one view, one React/Angular/Vue app, with one bloated app.js file), however I'm attempting to utilize the magic of JWT with a slightly separate structured application.

Structure

Rather than serving up one blade.php view from my Laravel app that garners one Vue app and instance, I'm attempting to serve up TWO separate blade.php views, that each operate as their own separate Vue SPA: one for the exterior of the application (pre-auth) and another for the interior of the app (post-auth).

Current State of App

To power my app's authentication system, I've utilized Tymon's jwt-auth lib ( a beautiful lib btw ) and tie everything together on the front with (as previously stated) Vue/Vuex. Everything works as expected, in my Register and Login components I'm able to hit my api, get a JWT in response, store it locally then annex said token into my Axios headers allowing all subsequent requests to harbor this token.

Dilemma

Now I'm at a crossroads. The post-auth route/view that I want to serve up is protected by a custom JWT middleware that redirects if a valid token is not presented:

Route::get('/home', 'Auth\HomeController@home')->middleware('jwt');

middleware

class JWT
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        JWTAuth::parseToken()->authenticate();

        return $next($request);
    }
}

and my pre-auth view and all its routes are protected by Laravel's native guest RedirectIfAuthenticated middleware, which is Guarded by JWT now:

class RedirectIfAuthenticated
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->check()) {
            return redirect('/home');
        }

        return $next($request);
    }
}

Questions

So this begs down to the following questions:

1) after a successful register/login on the front-end and a JWT is generated, stored locally and in Axios headers, How do I then redirect to my post-auth route with this valid token available?

2) How do I then make sure that Valid JWT persist and is present when the guest routes are hit to successfully redirect back to my post-auth route?

I'd prefer to keep all redirects and persistance checks on the backend if feasible

Jared Garcia
  • 751
  • 1
  • 8
  • 17
  • Are both of these applications on the same domain or on different domains? – Lassi Uosukainen Mar 22 '18 at 15:50
  • Same domain, Default Laravel Installation/Project with Vue on the front-end @Lassi Uosukaimen – Jared Garcia Mar 22 '18 at 16:07
  • So the issue is that you don't have the token available after redirecting to another page? Or indeed just refreshing the same page? – tobbr Mar 22 '18 at 16:15
  • Precisely, the token is generated on the backend and sent to the front end to be stored locally (in browser storage) and added to axios headers so every subsequent ajax requests is sent with token @tobbr – Jared Garcia Mar 22 '18 at 16:26
  • 1
    Store the token in cookies, not localstorage. Security, etc. Set the timeout for the JWT to match the timeout for Laravel's session. Check if the token is expired in middleware on each request, if it does expire, redirect them to the login page. Send the bearer token from the cookie with each subsequent axios request. – Ohgodwhy Mar 22 '18 at 16:52
  • is there any way you can even slightly, remotely, distinctly show that in code? :} @Ohgodwhy – Jared Garcia Mar 22 '18 at 17:08
  • i want every request should have token even form different controller, how can i do it? – Ali Raza Sep 16 '19 at 14:07

3 Answers3

0

So there are a couple of ways you can make sure the JWT token is available everywhere for Axios or indeed any frontend to use.

The most common way is to store the token in either a cookie or in the Web Storage of the browser (localStorage / sessionStorage)

The difference between localStorage and sessionStorage is that data stored in localStorage persists through browser sessions, sessionStorage is cleared when the page session ends.

The general consensus is that cookies are slightly more secure because they have a smaller attack vector, though neither method is completely secure. If you want to go more in depth you can start by reading this article.

To get more specific concerning your problem, first you want to setup token storage using one of the methods explained above, recommended method is cookies, you can find examples of how to do it with pure Javascript here.

Now that you have the token on every page you can redirect the user whichever way you like. Though I would suggest that instead of using your own middleware for JWT authentication, you can use the one that the JWT library provides: jwt.auth.

This middleware will automatically respond with error codes if something is wrong with the token, if there is it will return one of the following HTTP responses:

  • token_not_provided
  • token_expired
  • token_invalid
  • user_not_found

If one of these responses are returned (or if the request status code is 400) you can simply use the frontend to redirect the user back to your pre-auth routes.

When signing in, after saving the token to a cookie, use the frontend to redirect to the post-auth routes.

I know you said you wanted to keep redirect logic in the backend but that doesn't really make sense when you're for example calling the API when you're signing in, you can't really both return the token and cause a redirect at the same time from just the backend.

UPDATE

Very simple example of how you can authenticate with only guard and still get a token for the API. Borrowing from the redirect example from @Ohgodwhy, you can put the following inside your RedirectIfAuthenticated middleware.

public function handle($request, Closure $next, $guard = null)
    if (Auth::guard($guard)->check()) {
        if ((\Cookie::get('access_token') == null)) {
            $cookie = \Cookie::make(
                'access_token',
                \JWTAuth::fromUser(Auth::user()),
                config('session.lifetime'),
                null,
                $request->refeerer,
                false, // to make the cookie available in javascript
                false // to make the cookie available in javascript
            );

            return redirect('/home')->cookie($cookie);
        } else {
            return redirect('/home');
        }
    }

    return $next($request);
}

Just make sure that your $redirectTo in app/Http/Controllers/Auth/LoginController.php is set to a path that implements the RedirectIfAuthenticated middleware.

tobbr
  • 2,046
  • 3
  • 14
  • 15
  • i mean can't i though? in my `register` or `login` methods in my backend, upon successful authentication and generation of a token, can't i just call a redirect to my `post-auth` route adding in a custom header whose value is the token? – Jared Garcia Mar 22 '18 at 17:42
  • Not as far as I know, however instead of returning the cookie in the response you can use the approach @Ohgodwhy described to set the cookie from the backend instead, then you won't need to return it. – tobbr Mar 22 '18 at 18:05
  • yes but that still doesn't answer the question of redirecting to my post-auth route with that token present in either the form of header or form data. And even if i do redirect on front-end, how would i (in JS) make a request with a custom header? – Jared Garcia Mar 22 '18 at 18:07
  • Oh well in that case why not just modify the `RedirectIfAuthenticated` middleware to redirect to your pre-auth routes if the guard check fails? – tobbr Mar 22 '18 at 18:22
  • To clarify, what i mean is use Laravels native guard to handle access to the `/post-auth` and `/pre-auth` views and use JWT to handle access to the API. – tobbr Mar 22 '18 at 18:31
  • well because that'll be using two authentication guards and that has to be , i believe, pretty inefficient. ill be using laravel's native guard to authenticate then once in post auth to access any API routes ill have to basically authenticate again with JWT to make subsequent requests... – Jared Garcia Mar 22 '18 at 18:36
  • 1
    Not really since you can use `$token = JWTAuth::fromUser(Auth::user());` to get a token for a user that has authenticated with the guard. Thats what JWT does in the background. But if you don't want to do it that way then i supposed you have to do `JWTAuth::setToken($request->cookie('access_token'))->authenticate();` in your jwt middleware instead instead of parsing it from the request. – tobbr Mar 22 '18 at 18:53
  • if you can briefly demonstrate authentication logic implementing Laravel's native guard initially to get from pre-auth view to post-auth view then using JWT rest of the way on post-auth (since its SPA) that would be awesome – Jared Garcia Mar 22 '18 at 19:03
  • So I ended up implementing a similar method to yours. check answer and critique where you see faults – Jared Garcia Mar 22 '18 at 20:27
  • i want every request should have token even form different controller, how can i do it? – Ali Raza Sep 16 '19 at 13:06
0

On successful login, you will have the token, let's say its called $jwt_token

You can redirect to the page you are protecting once authorized and set the cookie in the response:

return redirect('/home')->cookie(
    'access_token', //name
    $jwt_token,  //value
    config('session.lifetime'), //expiration in minutes (matches laravel)
    config('app.url'),  // your app url
    true // HttpsOnly
);

From here, Axios can have access to the cookie by parsing the cookies on the document and retrieving the access_token

let token = document.cookie.split(';') // get all your cookies
    .find(cookie => cookie.includes('access_token')) // take only the one that matches our access_token name
    .split('=')[1] // get just the value after =

// terrible code example above for you

Now you can use this in your Axios requests by adding it as the value to Bearer in the Authorization header:

Authorization: `Bearer ${token}`

Your JWT middleware already leverage the authenticate method and therefore it should be handling the expiry for you as it stands:

JWTAuth::parseToken()->authenticate();

Under the hood this will attempt to validate the token's expiry based on the current TTL set in the config/jwt.php file. Given your work flow, I would also blacklist the token if it expires. You can add an Event Listener that listens for expired tokens and blacklists them by listening to Event::listen('tymon.jwt.expired');.

Please excuse any syntax errors, formatting issues or misspellings, I'm on my pone and will edit later to resolve those.

Ohgodwhy
  • 49,779
  • 11
  • 80
  • 110
  • yes, your redirecting approach after a login was in line with what i was thinking, however, the JWT middleware demands a token to be sent along with the requests to any route its protecting, so unless i can redirect to `/home` while setting the token in the header or sending the token somehow, ill never reach `/home`, just be redirecting back – Jared Garcia Mar 22 '18 at 17:40
  • i want every request should have token even form different controller, how can i do it? – Ali Raza Sep 16 '19 at 13:06
0

So here's the logic I ended up implementing:

In my LoginController.php login function, after successful authentication and generation of JWT, I return a response with Json and a new cookie, both with new token passed:

public function login(Request $request)
{
    $creds = $request->only(['email', 'password']);

    if (!$token = auth()->attempt($creds)) {
        return response()->json([
            'errors' => [
                'root' => 'Incorrect Credentials. Try again'
            ]
        ], 401);
    }

    return $this->respondWithToken($token);
}

protected function respondWithToken($token)
{
    return response()->json([
        'meta' => [
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]
    ], 200)
    ->withCookie(cookie('access_token', $token, auth()->factory()->getTTL()));
}

In my guest RedirectIfAuthenticated middleware check for cookie, if exists, setToken which in turn sets Guard to Authenticated and will always redirect to /home if token is available and valid:

public function handle($request, Closure $next, $guard = null)
{
    if ($request->hasCookie('access_token')) {
        Auth::setToken($request->cookie('access_token'));
    }

    if (Auth::guard($guard)->check()) {
        return redirect('/home');
    }

    return $next($request);
}

And In my post-auth Routes middleware I also setToken and if its valid and exists, will allow access, otherwise will throw a range of JWT errors which just redirect to pre-auth view:

public function handle($request, Closure $next)
{
    JWTAuth::setToken($request->cookie('access_token'))->authenticate();

    return $next($request);
}

Finally, I decided to handle redirection in the front-end being I'm using Axios which is promised based and can assure that cookie will be set before redirecting to post-auth view so no funny business happens! Cheers! Hope this helps anyone on their quest to Multi-Page SPA magic!

Jared Garcia
  • 751
  • 1
  • 8
  • 17