andrewzigler.com

Adding Auth0 to a Nuxt app

My latest web project required adding security and user authentication to a Nuxt app. The app is served as a static generated (pre-rendered) site on Netlify, which then "unfolds" into a full-featured single-page application upon loading. From then on, each successive route is explored on the live app in the user's browser and the user is therefore no longer navigating static files. This approach provides many benefits for SEO because the content of each discrete page is already embedded in a static generated file that represents the resource, and this improves both load times and Google's understanding of our website.

I wanted my guests to create accounts and interact with my new website using an identity. This would also facilitate testing and debugging by allowing me to toggle certain states onto some users (such as admins and beta testers). After some initial research, I settled on Auth0 and decided to use the official Nuxt auth-module. I went with Auth0 because it integrates easily with popular social media logins like Facebook, Google and more. User accounts are passwordless as a result, but you can also allow users to create accounts not tied to any pre-existing profiles. Auth0 will even host those user profiles in a database for you. With a free account you can handle up to 7,000 active users across two social media logins, which is more than enough for what I'm working on.

To begin, I installed the auth-module package and configured it for my application:

auth: {
    strategies: {
      auth0: {
        domain: 'YOUR_AUTH0_DOMAIN',
        client_id: 'YOUR_CLIENT_ID'
      }
    },
    redirect: {
      callback: '/login/redirect',
      home: '/profile'
    },
    watchLoggedIn: false
  }

I'm using slightly different pages to facilitate my redirects, so I defined them here. I have also toggled off a property that automatically redirects on logout. This was causing complications with my static pages, which I'll talk about below. Then on each page that I wanted to require user authentication, I include the middleware:

middleware: ['auth']

To initiate the login flow for a user, a button can use the Auth0 strategy we configured above with the module's auth instance, which is globally injected. You could even use the instance's properties to conditionally show the button to users that are not logged in, like so:

<v-btn v-show="!$auth.loggedIn" @click="$auth.loginWith('auth0')">Log in</v-btn>

Similarly, you can use the auth instance to facilitate logging the user out:

<v-btn v-show="$auth.loggedIn" @click="logout">Log out</v-btn>

But my approach for logging out ultimately involved wrapping that function and manually redirecting the user myself, to easily accommodate the project whether it's hosted locally or in production. The types of URLs the user might access the app from were also whitelisted in Auth0.

methods: {
    logout() {
      this.$auth.logout()
      window.location.href = `https://YOURAUTHDOMAIN.auth0.com/v2/logout?returnTo=${location.protocol.substr(
        0,
        location.protocol.length - 1
      )}%3A%2F%2F${location.host}`
    }
  }

As I mentioned above, I encountered some funky behavior when testing auth-module on my static pages. In dev mode, the app flow worked perfectly, even without using a logout wrapper or configuring it as to not automatically redirect the user. This was because the login flow redirected the user to Auth0 for authentication, and upon returning to the app they were actually presented with a static page. This static page would then leap into action, but it would lose the user's authentication in the process. The result was a broken login process when uploaded to Netlify, which was no good! To fix this, I implemented mounted event hooks on the login pages. This is the hook for the actual login page, which automatically redirects the user if they're authenticated and sitting on the login page:

 mounted() {
    // schedule a redirect depending on login status
    setTimeout(() => {
      // only redirect if a logged-in user is lingering here
      if (this.$route.path.includes('/login')) {
        if (this.$auth.user) {
          this.$router.push('/profile')
        }
      }
    }, 1.5 * 1000)
  }

When Auth0 redirects the user, it passes the authentication token in as a URL hash back to the website. Since the auth instance isn't instantiated yet upon returning to the site, we need to manually check the URL and retrieve that hash, which then gets passed into the auth instance. We then manually fetch the user with the auth instance and redirect if needed. This hook is for the login callback page, which is where the user lands upon being redirected back to the website from Auth0:

mounted() {
    if (this.$route.hash) {
      this.$auth.setToken(
        'auth0',
        `Bearer ${this.$route.hash.split('=')[1].split('&')[0]}`
      )
    }
    // fetch user information from token
    this.$auth.fetchUser()
    // schedule a redirect depending on authentication results
    setTimeout(() => {
      // only redirect if the user is still here
      if (this.$route.path.includes('/login/redirect')) {
        if (!this.$auth.user) {
          this.$router.push('/login')
        } else {
          this.$router.push('/profile')
        }
      }
    }, 1.5 * 1000)
  }

Now that the login flow is working, we can start using the authenticated user object to enrich our app. First, I created a "Provide user and app metadata" rule on Auth0 that will pass the user's metadata from Auth0 to the app so we can use it however we need. The user can view and edit their own metadata, and each user can also have app metadata that they cannot edit. User metadata can be for preferences or settings, and app metadata can be used for intrinsic properties and permissions. You can add metadata to a user in the Auth0 dashboard or via the API. Here is the Auth0 rule, which you can add in the dashboard from their website:

function (user, context, callback) {
  const namespace = 'YOUR_DOMAIN';
  context.idToken[namespace + 'user_metadata'] = user.user_metadata;
  context.idToken[namespace + 'app_metadata'] = user.app_metadata;
  
  callback(null, user, context);
}

With our new metadata, we can start building standard Vue functionality to compute values and display them within the app. Since I'm authenticating only with Facebook and Twitter, I have a simple check in place so I can quickly determine which of those sites the user logged in with. Auth0 gets the user's profile image from both sites, but I found the Facebook image to be very low quality and difficult to use. To rectify that, I created a computed property that will build a URL to a high-resolution version of that user's profile picture, based on their Facebook ID (which we receive as part of their authentication). Here are some ideas to get you started:

computed: {
    isAdmin(app) {
      if (
        app.$auth.user &&
        app.$auth.user['YOUR_DOMAIN/app_metadata'] &&
        app.$auth.user['YOUR_DOMAIN/app_metadata'].admin
      ) {
        return true
      } else {
        return false
      }
    },
    isFacebook: function(app) {
      if (app.$auth.user && app.$auth.user.sub.split('|')[0] === 'facebook') {
        return true
      } else {
        return false
      }
    },
    facebookImage: function(app) {
      return app.$auth.user
        ? 'https://graph.facebook.com/' +
            app.$auth.user.sub.split('|')[1] +
            '/picture?width=9999'
        : ''
    },
    name: function(app) {
      return app.$auth.user ? app.$auth.user.name : ''
    },
  }

Adding Auth0 to Nuxt ended up being a surprisingly complex and interesting task. This was my first time adding authentication to an app, and I found that most of the learning curve came from deciphering how authentication really works in the browser using cookies, and how that gets thrown into question when leveraging Nuxt to statically generate a site. I found more issues upon deploying the static files, which helped me further understand how it works under the hood. The power of Auth0 really surprised me, and I already have a wealth of ideas about how to use the authentication service for future applications and even games!