Shawn Wildermuth

Author, Teacher, and Filmmaker
.NET Foundation Board Member

Handling Token Authentication in Vue 3

Handling Token Authentication in Vue 3

So many of the Vue demos I've seen fail to look at the authentication use case. For some of my course demos I've had to dig into it.

I thought this might be a good place to share what I've learned as well as get my audience to code review what I'm doing. Thanks for the help!

If you want to follow along, feel free to grab the complete example:

https://github.com/shawnwildermuth/vuejwt

The minimal ASP.NET Core project exposes two APIs (one for authenticating and one for returning an array of colors). If you authenticate, it just returns a JWT:

[HttpPost]
public ActionResult<AuthResultModel> Post([FromBody] AuthRequestModel model)
{
  // NEVER DO THIS, JUST SHOWING THE EXAMPLE
  if (model.Username == "shawn@wildermuth.com"
    && model.Password == "P@ssw0rd!")
  {
    var result = new AuthResultModel()
    {
      Success = true
    };

    // Never do this either, hardcoded strings
    var token = TokenSecurity.GenerateJwt(model.Username);
    result.Token = new JwtSecurityTokenHandler().WriteToken(token);
    result.Expiration = token.ValidTo;

    return Created("", result);

  }

  return BadRequest("Unknown failure");
}

Please don't use any of the server code as an example as it's a minimal JWT implementation to just test the Vue section.

In the client directory, there is a Vue 3 project. That's where we'll focus. First we need a Login page:

<template>
  <div>
    <h1>Login</h1>
    <form novalidate @submit.prevent="onSubmit()">
      <div class="form-group">
        <label for="username">Username</label>
        <input type="text" name="username" v-model="model.username" class="form-control" />
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password" v-model="model.password" class="form-control" />
      </div>
      <div class="form-group">
        <input type="submit" class="btn btn-success" value="Login" />&nbsp;
        <router-link class="btn btn-info" to="/">Cancel</router-link>
      </div>
    </form>
  </div>
</template>

<script>
  import { reactive } from "vue";
  import store from "@/store";

  export default {
    setup() {

      const model = reactive({ username: "", password: ""});

      function onSubmit() {
        store.dispatch("login", model);
      }

      return {
        model,
        onSubmit
      }
    }
  }
</script>

Note that all this is doing is taking our model and sending it to Vuex to do the actual authentication. So this is just a simple form. All the real magic is in the Vuex store:

actions: {
  login: async ({ commit }, model) => {
    try {
      commit("setBusy");
      commit("clearError");
      const http = createHttp(false); // unsecured
      const result = await http.post("/api/auth", model);
      if (result.data.success) {
        commit("setToken", result.data);
        router.push("/");
      }
      else {
        commit("setError", "Authentication Failed");
      }
    } catch {
      commit("setError", "Failed to login");
    } finally {
      commit("clearBusy");
    }
  },
}

In this action, I'm just calling the service with post with the username/password. If it succeeds, I'm storing the token (in Vuex as well). Setting the token actually stores, the token and the expiration:

  mutations: {
    // ...
    setToken: (state, model) => {
      state.token = model.token;
      state.expiration = new Date(model.expiration)
    }
  },

Then we can just have a getter that returns whether we're logged in:

  getters: {
    isAuthenticated: (state) => {
      return state.token.length > 0 &&
        state.expiration > Date.now();
    }
  }, 

Notice that the getter is testing both that we have a token and that the expiration hasn't lapsed. There isn't a magic way to re-login as this expiration gets close. I'd suggest not keeping the credentials in the Vuex object to re-authenticate as that's a pretty big security hole. I would just redirect to the login page next time the user needs. But that assumption is really based on your specific use cases. There are some tricks you can do on the server side with sliding the expiration, on every authenticated call, but it shouldn't be used in high security situations.

So now we have a way to log in, what do we do with it? That's where Routing comes in. We have a simple set of routes to three pages (including Login):

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/colors',
    name: 'Colors',
    component: Colors,
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
]

But we want to protect certain pages if they are not authenticated. We can do this what a Guard. A guard is a small piece of code that is run during the routing pipeline. In our case we want it to be run before the route is executed:

const authGuard = (to, from, next) => {
  if (store.getters.isAuthenticated) {
    next();
  } else {
    next("/login")
  }
};

This method takes where the route is going to, coming from, and finally a function (next) to call to either call the route or re-route. In our case, if it's authenticated, we just call the next to move to where the route wants to go. But if it isn't we redirect it to the login page. Once we have this function, we can apply it on the paths necessary:

  {
    path: '/colors',
    name: 'Colors',
    component: Colors,
    beforeEnter: authGuard
  },

This way if you go to colors before you're authenticated, we reroute you to the login page. This example doesn't handle actually redirecting to colors after you login but you could do that easily. In my case, whenever login happens, we redirect to the root of the Vue project:

const result = await http.post("/api/auth", model);
if (result.data.success) {
  commit("setToken", result.data);
  router.push("/");
}

The call to router.push("/")> is what does the redirection.

Ok, we now have our routes protected from people that aren't logged in, but how do we use the JWT token now that we have it. In this example I'm using axios for the network (but you could do something similar with fetch). In this case, I have a function that builds the http object that I use:

import axios from "axios";
import store from "@/store";


export default function createHttp(secured = true) {

  if (secured) {
    return axios.create({
      headers: { "Authorization": `bearer ${store.state.token}` }
    });
  } else {
    return axios.create();
  }
} 

If the createHttp is called without parameters (or true), then I add the authorization header from the store automatically. Otherwise, I just create one. Why do we need both? Well, the unsecured one is actually necessary to do the Login. That's why the default is to create a secured connection.

Hopefully this minimal example will get you comfortable with using Tokens in your own Vue projects. Let me know if you see a way to improve the example (or just throw me a PR).