Working with auth0-spa-js locally

Is there a recommend path for working with auth0-spa-js locally? I naively thought I could just work with “localhost” as the hostname, but encountered a problem getting tokens silently. I eventually found a thread where someone else was having the same problem and the solution suggested was adding a custom entry to my /etc/hosts file that points at 127.0.0.1. So, I did that, but now auth0-spa-js doesn’t work because it’s not run on a secure origin. The error message helpfully points me at a resource explaining the problem, but it suggests I should be using the hostname “localhost”.

I’m sure I’m not the first person to attempt to make an API call when using auth0-spa-js during local development. But, the solutions for the two problems I’ve encountered are at odds with one another. Unfortunately, the “Work with Auth0 Locally” makes no mention of either problem. I’ve had to cobble it all together from community posts, and since I’m new to Auth0 in general, I’m certain I’ve overlooked something.

My current conclusion is that my only two options available are setting up SSL termination for my local webpack dev server or deploying everything to a hosting provider and testing against that. Neither option is terribly palatable, but if they’re all I can do, I’ll cope. Is that accurate? Or is there a more straightforward way to develop locally?

Hi @auth05,

Welcome to the Auth0 Community Forum!

Can you tell us a bit about what you are trying to do and what your setup is? Are you following any tutorials or quickstarts?

Let us see what we can do.

Thanks,
Dan

Hi Dan,

I’ve been following the React Quick Start guide, using the react-auth0-spa.js code from that page with the new universal login. This is a completely greenfield application. I have never used Auth0 prior to this, so I’m not clinging to any old habits. I’m just following the recommend path as far as I can tell. So, up to this point I’ve been using localhost and I’m able to login no problems. However, I’m unable to get a JWT for making an API call.

I’m fetching an access token for a GraphQL backend registered with my account. When I specify anything other than the default audience in the Auth0Provider config, I’m unable to get a token. Despite already being logged in, the getTokenSilently call always fails telling me “login_required”. Chasing that down brought me to the first post I linked to, where an Auth0 employee helpfully pointed out that consent can’t be skipped for localhost. Fair enough. I followed the directions and added an entry to /etc/hosts file pointing at 127.0.0.1. I had to change the way I invoke webpack-dev-server, too, because by default it performs Host header checks and fails with anything other than localhost.

During the course of that thread, the Auth0 employee emphatically states “you should avoid using localhost for development purposes” and went on to clarify that using the custom host pointing at 127.0.0.1 is how you should be developing with Auth0. So, I added the host and started working with that, but now the SPA code from Auth0 doesn’t work because I’m not using localhost. The error message in Chrome looks like:

Uncaught (in promise) Error: 
      auth0-spa-js must run on a secure origin.
      See https://github.com/auth0/auth0-spa-js/blob/master/FAQ.md#why-do-i-get-error-invalid-state-in-firefox-when-refreshing-the-page-immediately-after-a-login 
      for more information.
    
    at eval (auth0-spa-js.production.js:1)
    at eval (auth0-spa-js.production.js:1)
    at Object.eval [as next] (auth0-spa-js.production.js:1)
    at eval (auth0-spa-js.production.js:1)
    at new Promise (<anonymous>)
    at r (auth0-spa-js.production.js:1)
    at eval (auth0-spa-js.production.js:1)
    at initAuth0 (react-auth0-wrapper.jsx:28)
    at eval (react-auth0-wrapper.jsx:49)
    at commitHookEffectList (react-dom.development.js:19986)

Following the FAQ link in the error message, I’m informed that my custom host is not considered a secure origin, and so the Web Cryptography API won’t work. Just to avoid the reader needing a second tab open, the FAQ indicates that with some browser vendor variation, the default set of secure origins is:

(https, *, *)
(wss, *, *)
(*, localhost, *)
(*, 127/8, *)
(*, ::1/128, *)
(file, *, —)

So, anything over HTTPS is fine and HTTP with localhost is fine. But, Auth0 won’t let me use localhost and get an access token for a registered API. Moreover, it looks like Auth0 doesn’t want you developing with localhost as the hostname at all, but that seems to be at odds with what the Web Cryptography API requires.

Please let me know if you need any additional information. I’m really mostly sticking to the quick start code and trying to get an access token for working with a GraphQL API. Reproducing should be straightforward, but I can appreciate if you don’t want to run through the steps. Each issue I’ve hit I’ve found documentation for, I just don’t see how they all can be reconciled for local development.

Thanks,
Kevin

1 Like

@auth05,

Thanks for the detailed write up on this. I reached out to some more senior folks to see if there is a way around this. Having a self signed cert so you can use 127.0.0.1 over https may work, but I don’t know the ramifications and would like to get some more seasoned input.

I’ll report back with more.

Hi Kevin.
I think Joao, in the Community thread you link to, says that you should avoid localhost if you don’t want the consent page.

I’m not quite sure, however, that the consent page is required here (you would get a consent_required instead of login_required if the consent step is preventing Auth0 from silently issuing a token).

Are you specifying the audience in the options, along with the domain and the clientId?

Can you try getTokenWithPopup() to get the access token? I’m curious if you get a login prompt (as if the session is ending too son, or cookies aren’t being sent), a consent screen (which should be required only once), or something different.

1 Like

Hi Nicolas,

Thanks for the reply. Your comment pushed me along trying a few different things. getTokenWithPopup didn’t work either, but for a different reason. In this case, I was getting an error reading from a cache within auth0-spa-js.production.js:

            t.prototype.getTokenWithPopup = function(t, e) {
                return void 0 === t && (t = {
                    audience: this.options.audience,
                    scope: this.options.scope || this.DEFAULT_SCOPE
                }),
                void 0 === e && (e = Cn),
                r(this, void 0, void 0, function() {
                    return o(this, function(n) {
                        switch (n.label) {
                        case 0:
                            return t.scope = Fn(this.DEFAULT_SCOPE, this.options.scope, t.scope),
                            [4, this.loginWithPopup(t, e)];
                        case 1:
                            return n.sent(),
                            [2, this.cache.get({
                                scope: t.scope,
                                audience: t.audience || "default"
                            }).access_token]
                        }
                    })
                })
            }

The error is on the call to access_token there. In this case, the cache is returning undefined. I think that’s probably something worth cleaning up since a cache miss probably shouldn’t be catastrophic. But, the real problem is the t.audience value is set to "default", whereas the lone cache entry uses the audience value I specified in the provider.

This led me to try specifying the audience attribute in both the getTokenWithPopup and getTokenSilently calls. With that in place, things appear to be working now.

Just to provide a quick recap, I’ve been following the API calling section of the React SPA quick start guide. That guide shows specifying the audience only on the provider. The call to getTokenSilently does not pass the audience. I assumed that meant the audience value was inherited, but it looks like that’s not the case. I have to believe the docs worked for someone, so I don’t entirely understand why it’s not for me.

So, the full truth table for this is:

Provider Audience Token Fetch Method Token Fetch Audience Result
None specified getTokenSilently None specified Opaque access token
None specified getTokenSilently API audience Opaque access token
None specified getTokenWithPopup None specified “Invalid state” error from loginWithPopup definition
None specifed getTokenWithPopup API audience “Invalid state” error from loginWithPopup definition
API audience getTokenSilently None specified The “login_required” error I mentioned
API audience getTokenSilently API audience Functioning JWT
API audience getTokenWithPopup None specified The bogus cache read error I mentioned
API audience getTokenWithPopup API audience Functioning JWT

So, 10 hours later I’m happy to have a way to authorize my GraphQL backend. Thanks for pointing me in the right direction. I really don’t think there’s anything particularly unique about my environment here. This looks like maybe the docs and the library fell out of sync somewhere along the way. I think the error messages could be improved to help guide the correct solution as well.

There’s still the open question of what would happen if I tried to get a JWT for another API, since that audience value would not be able to match the audience specified in the provider config (since the provider audience would have to match the first API’s audience). But, it’s not something I need now so I won’t spend the time figuring that out.

Hi again Kevin.

I appreciate your detailed insights and description of the journey, including the docs you followed. I’m not prepared to comment on specifics yet because I want to try this on my side first (I’ve tried the “vanilla” auth0-spa-js sample but not the React quickstart), and analyze the resulting HTTP interactions with each combo (I still can’t explain the “login_required” method).

Rest assured that all this feedback won’t go to waste. We need this to work seamlessly and it needs to happens within minutes, not hours. This is very valuable to improve the experience.

I might not be able to come back to this until next week, but I’ll keep you posted.

1 Like

Hi!
I tried to reproduce the issue today, but for some reason everything went fine for me.
This is what I did after downloading the quickstart project:

  • I added the audience entry in auth_config.json
  • I added the audience entry in the render code of the Auth0Provider in index.js:
ReactDOM.render(
  <Auth0Provider
    domain={config.domain}
    client_id={config.clientId}
    redirect_uri={window.location.origin}
    onRedirectCallback={onRedirectCallback}
    audience={config.audience}
  >
    <App />
  </Auth0Provider>,
  document.getElementById("root")
);
  • I added some crude code in the Hero.js view to get a token:
// this is new
import { useAuth0 } from "../react-auth0-spa";

const Hero = () => {
  // this is new
  const { getTokenSilently,getTokenWithPopup } = useAuth0();
  // this is new
  const getAccessToken = async function() {
    try {
    var token = await getTokenSilently({audience:"Test"});
    alert(token);
    } catch (e)
    {
      alert(JSON.stringify(e));
    }
  }
    return (
  
      <div className="text-center hero my-5">
        <img className="mb-3 app-logo" src={logo} alt="React logo" width="120" />
        <h1 className="mb-4">React.js Sample Project</h1>

        <p className="lead">
          This is a sample application that demonstrates an authentication flow for
          an SPA, using <a href="https://reactjs.org">React.js</a>
        </p>
        <button onClick={() => getAccessToken()}>Let's get an Access Token</button>
      </div>
    )
  };

export default Hero;

In the above code I reference the useAuth0() export from react-auth0-js to call the API (this is important, to get the default options), and there’s a button at the bottom that triggers the “getAccessToken()” local function.

I tried putting the audience as the provider configuration, and that works as expected (the token obtained from both getTokenSilently and getTokenWithPopup are for that audience). I also tried not configuring an audience for the provider and instead passing the audience in the calls to getTokenSilently and getTokenWithPopup and that also worked as expected.

From the issues you describe, it seems as if at some point there was more than one instance of the Auth0Provider instead of using the one stored as a React context and get by useAuth0().

Would you mind starting fresh and doing the above changes to see if you get a good result? If so, can you spot the differences that caused trouble?

Hi Nicolas,

Thank you for taking the time to look at this. I started from scratch and indeed couldn’t reproduce the problem. Trying to compare this code against the app where I had with the failure, it looks like the problem came down to getTokenSilently({}) not doing the same thing as getTokenSilently(). I suspect at some point when I was trying to make the audience work, I just deleted the property rather than the entire arg, thinking the argument would be merged with the defaults.

I’m a little uneasy attributing all the problems I’ve been having entirely to that because I didn’t start trying to specify an audience (and thus, pass an argument at all) in the getTokenSilently call until after I was already having problems getting the token. But, the table of error states I presented previously appears to only hold true when passing an empty object as the argument to getTokenSilently. As for the original problem, maybe webpack did something unexpected, a stale build stuck around, or an older version of auth0-spa-js had an issue. I really don’t know, but I’m having difficulties reproducing the initial error to verify, since I’ve blown away node_modules, upgraded auth0-spa-js, and made myriad other changes while trying to track down the problem.

In any event, I’m happy to have it working now. And thank you very much for taking the time to help me work it out. I think there may still be some opportunities to improve either the aesthetics or the error messages around this call. I’m happy to attribute a getTokenSilently({}) call to user error, but picking a completely different audience ("default") in that case and resulting in errors far removed from the source of the issue isn’t ideal. I appreciate you can’t protect users from all the horrible things they may do. Perhaps argument validation or merging the argument with the provider’s values would be a worthwhile compromise.

1 Like