Tuesday, 4 August, 2020 UTC


Summary

Authentication is hard. Even if you know the ins and outs of it, handling registration, login, email verification, forgotten password, secret rotation... and what not... is a tedious work.
For this reason, we use auth providers such as AWS Cognito or Auth0. But this comes with its own drawback, namely that you are at the provider's mercy when it comes to examples and tutorials. If a resource you need does not exist, you either need to contact support and wait for them (but nobody got time for that), or figure it out yourself by the good ol' trial and error method.
A couple of days ago, I had to use Auth0 with Vue.js and TypeScript. Now, Auth0 has an excellent tutorial for Vue.js, but I could not find any examples in TypeScript. So seeing no better option, I started annotating the code provided by the tutorial.
I finished it, and in this blogpost, I'll walk you through the details, so you don't have to repeat this chore.
We will follow the original Auth0 Vue tutorial structure which can be found here. To make it easier to compare the two, we'll use the exact same first-level headings as the original.
You can find my complete auth0 vue typescript repo on RisingStack's Github.
Configure Auth0
First, you'll need to set up your Auth0 application. That part is very well written in the original tutorial, and I would like to be neither repetitive nor plagiarize Auth0's content, so please go ahead and read the first section there, then come back.
Create a Sample Application
Now we already start to diverge from the Auth0 tutorial.
If you already have an existing app, make sure that typescript, vue-class-component, and vue-property-decorator are present in your package.json, as we'll use class components.
If you don't have one, let's create a sample app.
$ vue create auth0-ts-vue
When prompted, select Manually select features.
We'll need Babel, TypeScript, and Router.
The next 3 questions are about deciding whether you want to use class-style component syntax, Babel, and history mode. Hit enter for all three to answer "Yes". You might opt-out from history mode if you really want to.
It is entirely up to you if you want to use dedicated config files or not, and if you want to save this as a preset.
Grab a beverage of your preference while the dependencies are being installed.

Install the SDK

Once it's done, we need to install our auth0 dependencies.
$ cd auth0-ts-vue-example
$ npm install @auth0/auth0-spa-js
The auth0-spa-js package comes with its own type definitions, so we're all set for now.

Modify your Webpack Config

If you followed the original Auth0 tutorials configuration part, you've set up your URLs to listen at port 3000. Time to hard code this into our webpack dev-server.
Create a vue.config.js file in the root directory of your app.
const webpack = require('webpack')

module.exports = {
  devServer: {
    port: 3000
  }
}
This way, we don't have to specify the PORT env var when we run our app. We'd need to change it in Auth0 anyway all the time, while we're developing it.

Start the application

$ npm run serve
Leave it running so we can leverage Webpack's incremental build throughout the process.
Create an Authentication Wrapper
Have you ever created a Vue.js plugin? Well, now is the time!
The easiest way to use Auth0 in your app is to make it available on this in each of your components, just as you do with $route after you've installed Vue Router.
It would be nice if this was a separate dependency, but for the sake of simplicity, let it live inside our codebase.
Create a directory called auth inside your src dir then create the following files: index.ts auth.ts, VueAuth.ts, User.ts. The original tutorial has them all in one file. Still, in my opinion, it is easier to understand what's happening if we separate the matters a bit, and it also results in nicer type definitions too.
Our index.ts will be a simple barrel file.
export * from './auth'
auth.ts is where we define the plugin. VueAuth.ts is a wrapper Vue object around auth0-spa-js, so we can leverage the observability provided by Vue, and User.ts is a class to make its type definition nicer.

Defining our User

Let's go from the inside out and take a look at User.ts
import { camelCase } from 'lodash'

export class User {
  sub: string
  names: string
  nickname: string
  picture: string
  updatedAt: string
  email: string
  emailVerified: boolean

  provider?: string
  id?: string

  givenName?: string
  familyName?: string
  locale?: string
  [key: string]: string | boolean | undefined

  constructor (auth0User: { [key: string]: string | boolean | undefined }) {
    if (!auth0User) return
    for (const key in auth0User) {
      this[key] = auth0User[key]
    }

    this.sub = auth0User.sub as string
    this.provider = this.sub.split('|')[0]
    this.id = this.sub.split('|')[1]
  }
}
Now, this requires a bit of explanation. The first block of fields are the one that are always present, no matter what login scheme the user used. Sub is the OpenID ID Token's Subject Identifier, which contains the authentication provider (eg. auth0 or google) and the actual user id, separated by a |. The other mandatory fields are probably self-explanatory.
Next are provider and id, which are a result of splitting sub, so they should be there, but we cannot be sure. The last are the ones that were only present when Google OAuth is used as the provider. There might be more, depending on what connections you set up and what other data you request. Or you could even code custom fields in the returned ID Token... but I digress.
Last we tell TypeScript, that we want to be able to use the bracket notation on our object by adding [key: string]: any
Our constructor takes a raw user object with similar fields but snake_cased. That's why we camelCase them and assign each of them to our User object. Once we're done, we extract the provider and the id from the subfield.

Show me the Wrapper

Time to take a look at VueAuth.ts
import { Vue, Component } from 'vue-property-decorator'
import createAuth0Client, { PopupLoginOptions, Auth0Client, RedirectLoginOptions, GetIdTokenClaimsOptions, GetTokenSilentlyOptions, GetTokenWithPopupOptions, LogoutOptions } from '@auth0/auth0-spa-js'
import { User } from './User'

export type Auth0Options = {
  domain: string
  clientId: string
  audience?: string
  [key: string]: string | undefined
}

export type RedirectCallback = (appState) => void


@Component({})
export class VueAuth extends Vue {
  loading = true
  isAuthenticated? = false
  user?: User
  auth0Client?: Auth0Client
  popupOpen = false
  error?: Error

  async getUser () {
    return new User(await this.auth0Client?.getUser())
  }

  /** Authenticates the user using a popup window */
  async loginWithPopup (o: PopupLoginOptions) {
    this.popupOpen = true

    try {
      await this.auth0Client?.loginWithPopup(o)
    } catch (e) {
      console.error(e)
      this.error = e
    } finally {
      this.popupOpen = false
    }

    this.user = await this.getUser()
    this.isAuthenticated = true
  }

  /** Authenticates the user using the redirect method */
  loginWithRedirect (o: RedirectLoginOptions) {
    return this.auth0Client?.loginWithRedirect(o)
  }

  /** Returns all the claims present in the ID token */
  getIdTokenClaims (o: GetIdTokenClaimsOptions) {
    return this.auth0Client?.getIdTokenClaims(o)
  }

  /** Returns the access token. If the token is invalid or missing, a new one is retrieved */
  getTokenSilently (o: GetTokenSilentlyOptions) {
    return this.auth0Client?.getTokenSilently(o)
  }

  /** Gets the access token using a popup window */
  getTokenWithPopup (o: GetTokenWithPopupOptions) {
    return this.auth0Client?.getTokenWithPopup(o)
  }

  /** Logs the user out and removes their session on the authorization server */
  logout (o: LogoutOptions) {
    return this.auth0Client?.logout(o)
  }

  /** Use this lifecycle method to instantiate the SDK client */
  async init (onRedirectCallback: RedirectCallback, redirectUri: string, auth0Options: Auth0Options) {
    // Create a new instance of the SDK client using members of the given options object
    this.auth0Client = await createAuth0Client({
      domain: auth0Options.domain,
      client_id: auth0Options.clientId, // eslint-disable-line @typescript-eslint/camelcase
      audience: auth0Options.audience,
      redirect_uri: redirectUri // eslint-disable-line @typescript-eslint/camelcase
    })

    try {
      // If the user is returning to the app after authentication..
      if (
        window.location.search.includes('error=') ||
        (window.location.search.includes('code=') && window.location.search.includes('state='))
      ) {
        // handle the redirect and retrieve tokens
        const { appState } = await this.auth0Client?.handleRedirectCallback() ?? { appState: undefined }

        // Notify subscribers that the redirect callback has happened, passing the appState
        // (useful for retrieving any pre-authentication state)
        onRedirectCallback(appState)
      }
    } catch (e) {
      console.error(e)
      this.error = e
    } finally {
      // Initialize our internal authentication state when the page is reloaded
      this.isAuthenticated = await this.auth0Client?.isAuthenticated()
      this.user = await this.getUser()
      this.loading = false
    }
  }
}

It might make sense to compare this with the original tutorial.
In the original tutorial, a Vue object is created while we're creating a class to make its annotation easier. There you can find it as:
  // The 'instance' is simply a Vue object
  instance = new Vue({
    ...
  })
Now let's unpack it.
First, we need to import a couple of types, including our User class.
Then we create the Auth0Options and RedirectCallback type aliases for convenience.
Instead of creating a simple Vue object, we define a Class Component. The public fields are the same as the data object in the original, whereas the static ones are the parameters passed to the plugin.
We differ in two substantial way from the original tutorial:
  1. We have one less method: handleRedirectCallback is not used anywhere in the original, so we omitted it.
  2. Instead of setting up the Auth0 Client in the Vue object's created hook, we use a separate method called init. Aside from that, the contents of the two are identical.
The reason for using a separate method is simple: The created hook is used in place of a constructor when it comes to Class Components, as the constructor of the class is usually called by Vue.
First, a component object is created just like when using Vue({}), passing it the data, methods, watchers, paramlist, and all the things we usually define for components. When this is done, the created hook is called. Later, when the component is actually used and rendered, the params are passed to it, and mounted, or updated.
The problem with the original one is that we cannot pass parameters to the created method. Neither can we write a proper constructor. So we need to have our own method we will call right after the object is instantiated just as it's done with created by Vue.
Let's dissect init a bit.
First, we create and auth0Client.
Then, in the try-catch block, we check if the user is returning after authentication and handle it. We check if the query params contain any signs of redirection. If they do, we call auth0Client.handleRedirectCallback, which parses the URL and either rejects with an error or resolves with and appState.
Then, we pass on the appState to onRedirectCallback. This is a function we can pass to the plugin when we install it to Vue, so we can handle the app level ramifications of a login.
For the other methods, getUser is a simple wrapper around the authClient's getUser method. We pass on the resolved promise to our User's constructor to create a nicely looking User object.
Next, there is loginWithPopup, which we won't use, as popups can be blocked by browsers. So we'll go with the redirect way, where the user is redirected to Auth0, login, then the callback URL is called by Auth0 passing information to our app in the callback URL's query.
The information in the URL is parsed by auth0Client.handleRedirectCallback which will return a Promise<RedirectCallbackResult>. The Promise will be rejected if there is an error in the authentication flow.
We have a couple of simple wrappers around the auth0Client. loginWithRedirect initiates the flow I described above, logout speaks for itself.
Finally, we set up the user and check if we're authenticated.

Let's turn this into a Plugin

Now, all we need to do is create a proper plugin.
If you take a look at Vue's documentation about plugins, you'll see that we need to create an object that exposes an install method. This method will be called when we pass the object to Vue.use and it will receive the Vue constructor and optionally... options.
type Auth0PluginOptions = {
  onRedirectCallback: RedirectCallback,
  redirectUri: string,
  domain: string,
  clientId: string,
  audience?: string,
  [key: string]: string | RedirectCallback | undefined
}

export const Auth0Plugin = {
  install (Vue: VueConstructor, options: Auth0PluginOptions) {
    Vue.prototype.$auth = useAuth0(options)
  }
}
In our install method, we add an $auth member to any Vue object, so the VueAuth object is available everywhere, just as vue-router is.
Let's implement the useAuth function.
/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname)

let instance: VueAuth

/** Returns the current instance of the SDK */
export const getInstance = () => instance

/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  redirectUri = window.location.origin,
  ...options
}) => {
  if (instance) return instance

  // The 'instance' is simply a Vue object
  instance = new VueAuth()
  instance.init(onRedirectCallback, redirectUri, options as Auth0Options)

  return instance
}
useAuth returns a singleton VueAtuh instance, and extracts the onRedirectCallback and redirectUri from the options object. What's left is an Auth0Options type which we'll pass on straight to the auth0Client.
You can see the init method in action we created earlier. Then VueAuth is instantiated if it hasn't been already. Above that, we also expose a getInstance function, in case we need to use it outside of a Vue component.
Let's see here the whole auth.ts for your copy-pasting convenience:
import { VueConstructor } from 'vue'
import { VueAuth, Auth0Options, RedirectCallback } from './VueAuth'

type Auth0PluginOptions = {
  onRedirectCallback: RedirectCallback,
  domain: string,
  clientId: string,
  audience?: string,
  [key: string]: string | RedirectCallback | undefined
}

/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = (appState) =>
  window.history.replaceState({}, document.title, window.location.pathname)

let instance: VueAuth

/** Returns the current instance of the SDK */
export const getInstance = () => instance

/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  redirectUri = window.location.origin,
  ...options
}) => {
  if (instance) return instance

  // The 'instance' is simply a Vue object
  instance = new VueAuth()
  instance.init(onRedirectCallback, redirectUri, options as Auth0Options)

  return instance
}

// Create a simple Vue plugin to expose the wrapper object throughout the application
export const Auth0Plugin = {
  install (Vue: VueConstructor, options: Auth0PluginOptions) {
    Vue.prototype.$auth = useAuth0(options)
  }
}
As you can see, we're extending the Vue constructor with a new instance member. If we try to access it in a component, the TypeScript compiler will start crying as it has no idea what happened. We'll fix this a bit later down the line.
Now, the Auth0Options are the ones that are needed for the client to identify your tenant. Copy the Client ID and Domain from your Auth0 applications settings and store them in a file called auth.config.json for now. It would be nicer to inject them as environment variables through webpack, but as these are not sensitive data, we'll be just fine like that as well.
With all that said, I will not include my auth.config.json in the reference repo, only an example you'll need to fill in with your data.
{
  "domain": "your tenant's domain",
  "clientId": "your app's clientId"
}
Make sure to add "resolveJsonModule": true, to your tsconfig.json.
Finally, we're ready to create our main.ts.
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { Auth0Plugin } from './auth'
import { domain, clientId } from '../auth.config.json'

Vue.use(Auth0Plugin, {
  domain,
  clientId,
  onRedirectCallback: (appState) => {
    router.push(
      appState && appState.targetUrl
        ? appState.targetUrl
        : window.location.pathname
    )
  }
})

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
The onRedirectCallback redirects the user to a protected route after they have authenticated. We'll cover this a bit later when we create an actual protected route.
Log in to the App
Time to put the authentication logic to use.
First, we'll add a Login / Logout button to Home.vue
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" />

    <!-- Check that the SDK client is not currently loading before accessing is methods -->
    <div v-if="!$auth.loading">
      <!-- show login when not authenticated -->
      <button v-if="!$auth.isAuthenticated" @click="login">Log in</button>
      <!-- show logout when authenticated -->
      <button v-if="$auth.isAuthenticated" @click="logout">Log out</button>
    </div>
  </div>
</template>
We'll also need to update the logic in the script tag of Home
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import HelloWorld from '@/components/HelloWorld.vue'

@Component({
  components: {
    HelloWorld
  }
})
export default class Home extends Vue {
  login () {
    this.$auth.loginWithRedirect({})
  }

  // Log the user out
  logout () {
    this.$auth.logout({
      returnTo: window.location.origin
    })
  }
}
</script>
First, we turn the original example component into a Class Component. Second, the methods simply call the methods of VueAuth exposed by our Auth0Plugin.
But what's that? this.$auth is probably underlined in your IDE. Or if you try to compile the code you'll get the following error:
Of course, we still have to tell the compiler that we have augmented the Vue constructor with our $auth member.
Let's create a shims-auth0.d.ts file in our src directory. If you're using VSCode, you might need to reload the window to make the error go away.
import { VueAuth } from './auth/VueAuth'
declare module 'vue/types/vue' {
  interface Vue {
    $auth: VueAuth
  }
}
Checkpoint
Now, let's try to compile our code. If you have configured your Auth0 credentials correctly, you should be redirected to the Auth0 Universal Login page when you click Login, and back to your app against once you have logged in.
Then, you should be able to click Log out and have the application log you out.
Display the User's Profile
So far so good, but let's try to create a protected route. Displaying the user's profile seems like a prime target for that.
Let's create a file called Profile.vue in src/views.
<template>
  <div>
    <div>
      <img :src="$auth.user.picture">
      <h2>{{ $auth.user.name }}</h2>
      <p>{{ $auth.user.email }}</p>
    </div>

    <div>
      <pre>{{ JSON.stringify($auth.user, null, 2) }}</pre>
    </div>
  </div>
</template>
That's it. We read all the information we need from $auth.user we've already set up in VueAuth.ts.

Add a route to the Profile component

Let's update the app's routing configuration, so the users can access their profile.
Open up src/router/index.ts and add the following to the routes array.
//.. other imports

// NEW - Import the profile component
import Profile from "../views/Profile.vue";

Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
  routes: [
    // .. other routes and pages ..

    // NEW - add the route to the /profile component
    {
      path: "/profile",
      name: "profile",
      component: Profile
    }
  ]
});

export default router
Now we need to update the navigation bar in App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
      <span v-if="$auth.isAuthenticated"> |
        <router-link  to="/profile">Profile</router-link>
      </span>
    </div>
    <router-view/>
  </div>
</template>
Checkpoint
The code should compile, so let's check if we can navigate to the Profile page and see the data. For added profit, try logging in with both Google and register a username and password. Take note of the data you get.
Secure the Profile Page
We have the route, time to make it protected. Let's create a new file in src/auth called authGaurd.ts.
import { getInstance } from './auth'
import { NavigationGuard } from 'vue-router'

export const authGuard: NavigationGuard = (to, from, next) => {
  const authService = getInstance()

  const fn = () => {
    // Unwatch loading
    unwatch && unwatch()
    
    // If the user is authenticated, continue with the route
    if (authService.isAuthenticated) {
      return next()
    }

    // Otherwise, log in
    authService.loginWithRedirect({ appState: { targetUrl: to.fullPath } })
  }

  // If loading has already finished, check our auth state using `fn()`
  if (!authService.loading) {
    return fn()
  }

  // Watch for the loading property to change before we check isAuthenticated
  const unwatch = authService.$watch('loading', (loading: boolean) => {
    if (loading === false) {
      return fn()
    }
  })
}
First, we put auth.ts's getInstance to use. Then we create a function that checks if the user is authenticated. If they are, we call next, otherwise redirect them to login.
However, we should only call this function, if the authService is not loading, as otherwise, we still don't have any settled information about the login process.
If it is still loading, we set up a watcher for authService.loading, so when it turns true, we call our guard function. Also, please notice that we use the unwatch function returned by $watch to clean up after ourselves in fn.
I personally prefer giving descriptive names to my functions, but I only wanted to change things for the sake of either type annotation, or stability, so forgive me for keeping fn as it is to maintain parity with the JS tutorial.
Guidance with Auth0, Vue & TypeScript
Auth0 and all other authentication providers relieve us from the tedious job of handling user management ourselves. Auth0 itself excels in having a lot of educational resources for their users. The original Vue tutorial was really helpful, but seeing that TypeScript is becoming the industry standard when it comes to writing anything that should be run by JavaScript runtimes, it would be nice to see more TypeScript tutorials.
I hope this article manages to fill in a bit of this gap. If you liked what you just read, please share it with those who might need guidance with Auth0, Vue & TypeScript!
Happy authenticating!