Auth0 client is null in Vue SPA component on page refresh

I have a Vue SPA based on one of Auth0’s quickstart apps (GitHub - auth0-samples/auth0-vue-samples: Auth0 Integration Samples for Vue.js Applications). Everything works fine out of the box, but as soon as I try using the Auth0 client in my component code I run into problems. I followed the “Calling an API” tutorial (Auth0 Vue SDK Quickstarts: Calling an API), which unhelpfully only shows how to call an API using a button. What I want to do is trigger an authenticated call to my API on initial page load so that I can ensure certain data exists in my own API (or create it if it does not). This seems like it should be pretty straightforward. I just throw this code in my created hook of my Vue component:

await this.$auth.getTokenSilently().then((authToken) => {
    // reach out to my API using authToken
});

This actually works fine if the app hot reloads from my npm dev server, it reaches out to my API, which authorizes the request using the token, and sends back the correct data. The problem is when I manually reload the page, which causes this:

Uncaught (in promise) TypeError: Cannot read property 'getTokenSilently' of null
    at Vue.getTokenSilently (authWrapper.js?de49:65)
    at _callee$ (App.vue?234e:49)

Inside the authWrapper.js file (where the Auth0 client lives), the function call is here:

getTokenSilently(o) {
    return this.auth0Client.getTokenSilently(o);
}

When I debug the call, “auth0Client” doesn’t exist, which is why it’s failing. What I can’t understand is the correct way to ensure it does exist before I attempt to make the call. There’s nothing in the samples that indicates the right way to do this. I tried putting my component code in different components and different Vue lifecycle hooks (created, beforeMount, mounted, etc), all with the same result. The client becomes available after 800 ms or so, but not when this code executes.

This is clearly a timing problem, but it’s not clear to me how to tell my component code to sit and wait until this.auth0Client is non-null without doing something horrible and hacky like a setInterval.

4 Likes

I encountered the same issue within one of my components (specifically, it was trying to grab the token to be used in a header for an Axios GET request in the component’s mounted() hook). I doubt this is best practice and would love to see an official recommendation from Auth0, but my workaround for the time being was the following within the component relying upon auth0 being loaded completely:

watch: {
    authLoaded(newVal) {
        if (newVal) {
            // At this point, we know that it's been initialized and is ready for use
            // Proceed with calling your method that requires use of auth0Client
            this.$auth.getTokenSilently()
        }
},
computed: {
    authLoaded() {
        return this.$auth.auth0Client
    }
}
2 Likes

I finally came up with a similar solution to yours:

import { getInstance } from "./auth/authWrapper";

// ... Vue component:
created() {
    this.init(this.loadTokenIntoStore);
},
methods: {
    init(fn) {
        // have to do this nonsense to make sure auth0Client is ready
        var instance = getInstance();
        instance.$watch("loading", loading => {
            if (loading === false) {
                fn(instance);
            }
        });
    },
    async loadTokenIntoStore(instance) {
        await instance.getTokenSilently().then((authToken) => {
            // do authorized API calls with auth0 authToken here 
            // or load authToken in state store for other components to use
        });
    }
}

This happens on root component load. At this point I put the token into my Vuex store and it’s available to all my other components (until page refresh, which forces a new token retrieval).

I think the underlying issue here is that I wasn’t supposed to use the Vue SPA quickstart if I’m using a separate back end. In such a case, it appears as though the right thing to use is a machine to machine flow, which I believe can drop an authentication token cookie into the SPA on the callback rather than using a bearer token obtained from an asynchronous Auth0 client initialization. This is not very clear from the existing documentation, and it would be helpful if Auth0 placed a warning statement about it at the very beginning. I’ve invested a lot of hours in getting this setup running as-is, and at some point now I have to go back and convert it to the other flow.

Thanks for your response, it looks a little cleaner than mine so I’ll probably give it a whirl!

2 Likes

Exactly what I needed, thanks for this!

Also +1 for this:

Binding tokens to global Axios instances is quite a popular usecase, maybe some kind of a blog post / article clarifiying this could be of help?

I think that would be really helpful, for sure. My main issue with Auth0’s documentation, despite how extensive and helpful it usually is, is that it’s not always clear what flow to use under certain circumstances, nor is it well explained how to integrate those flows with certain tech stacks, and there are inconsistent messages throughout.

For example, let’s say you have some sort of a Java back-end (Spring or Micronaut or something), but you want to attach it to an SPA of some kind. There’s no indication of the right way to do this. If you look at the SPA quickstarts, they all integrate with something like Express. You can find documentation elsewhere that explains how to use JWT authorization for Java back ends, but it’s not tied directly to the SPA documentation, making it unclear if that’s the intended use case. Meanwhile, if you look a the Spring quickstart, it has you using Thymeleaf templates as your front-end, with no mention of what to do if you want to connect an SPA to Spring–which seems like a pretty common use case to me!

From what I can gather, in such a case (SPA + Java back end) you’re supposed to implement the login and callbacks in server-side templates, and then stitch your SPA in by forwarding the callback redirect to the SPA where a cookie should be available. This, of course, requires that your back end be able to handle cookie authentication instead of bearer, among other details, few of which are all explained in one place.

tl;dr: It would be nice to get some solid recommendations and quickstarts in hybrid stacks.

3 Likes

I used the solution provided above and managed to put it into a single Vuex action. The whole Vuex module to those interested:

import { getInstance } from "@/auth";

const state = {
  token: null
};

const getters = {
  token(state) {
    return state.token;
  }
};

const mutations = {
  setToken(state, token) {
    state.token = token;
  }
};

const actions = {
  retrieveTokenFromAuthz(context) {
    return new Promise((resolve, reject) => {
      const instance = getInstance();
      instance.$watch("loading", loading => {
        if (loading === false && instance.isAuthenticated) {
          instance
            .getTokenSilently()
            .then(authToken => {
              context.commit("setToken", authToken);
              resolve(authToken);
            })
            .catch(error => {
              reject(error);
            });
        }
      });
    });
  }
};

export default {
  state,
  actions,
  mutations,
  getters
};

I call the action at the created() hook in App.vue.

Feedback appreciated.

3 Likes

Yeah that’s a solid way of doing it, pulling the boilerplate out of the root component and putting it in Vuex. I’ll probably use this. Thanks for the follow up!

I ran into this problem a couple of weeks back - didn’t see this thread. I didn’t fancy the overhead of use $watch to deal with this so went another way.

The fundamental problem is that the provided sample code is defining the created hook as async when Vue lifecycle hooks all have to be asynchronous. The workaround I used was to edit it to this

    async created() {
      // Create a new instance of the SDK client using members of the given options object
      this.auth0ClientPromise = createAuth0Client({
        domain: options.domain,
        client_id: options.clientId,
        audience: options.audience,
        redirect_uri: redirectUri
      })

      this.auth0Client = await this.auth0ClientPromise;

(+ the addition to data)

Then in the code that is using it on startup, I can do something like this

        mounted() {
            this.$auth.auth0ClientPromise.then(() => {
                this.autoLogin();
            });

        }
4 Likes

This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.