Unable to communicate between single page application and server

This is my current flow:

I have a single page application where the user logs in. When they log in I save their accessToken:

const accessToken = await auth0Client.getTokenSilently();

In the browser storage. Then I have a chrome extension which picks up the token from the browser storage. The exntesion needs to send a request to an API endpoint /generate (which I added in the API section of the auth0 app). The same server code is serving the SPA for login and the /generate API endpoint. But when I pass along with the authToken I received after logging in to the /generate endpoint it says that it’s invalid. If I use the token provided in the “Test” section, that works.

So I’m unable to understand why the token given by the SPA isn’t valid for authenticating with the API.

Here’s my server.js:

const express = require("express");
const cors = require("cors");
const { join } = require("path");
const app = express();
const { auth, requiredScopes } = require("express-oauth2-jwt-bearer");

const checkJwt = auth({
  audience: "http://localhost",
  issuerBaseURL: "https://dev-csc8n75wcojtwxmv.us.auth0.com/",
  tokenSigningAlg: "RS256",
});

app.use(express.json());
app.use(cors());

// Serve static assets from the /public folder
app.use(express.static(join(__dirname, "public")));

// Endpoint to serve the configuration file
app.get("/auth_config.json", (req, res) => {
  res.sendFile(join(__dirname, "auth_config.json"));
});

// Serve the index page for all other requests
app.get("/*", (_, res) => {
  res.sendFile(join(__dirname, "index.html"));
});

app.post("/generate", checkJwt, async (req, res) => {
  const token =
    req.headers.authorization && req.headers.authorization.split(" ")[1];
  console.log(token);
  // get/validate user details from token
  const { prompt } = req.body;

 // app specific code
});

// Listen on port 3000
app.listen(3000, () => console.log("Application running on port 3000"));

my app.js code:

let auth0Client = null;

const fetchAuthConfig = () => fetch("/auth_config.json");

const configureClient = async () => {
  const response = await fetchAuthConfig();
  const config = await response.json();

  auth0Client = await auth0.createAuth0Client({
    domain: config.domain,
    clientId: config.clientId,
  });
};

window.onload = async () => {
  await configureClient();
  updateUI();
  const isAuthenticated = await auth0Client.isAuthenticated();

  if (isAuthenticated) {
    // show gated content
    return;
  }

  const query = window.location.search;
  if (query.includes("code=") && query.includes("state=")) {
    await auth0Client.handleRedirectCallback();
    updateUI();
    window.history.replaceState({}, document.title, "/");
  }
};

const updateUI = async () => {
  const isAuthenticated = await auth0Client.isAuthenticated();

  document.getElementById("btn-logout").disabled = !isAuthenticated;
  document.getElementById("btn-login").disabled = isAuthenticated;

  if (isAuthenticated) {
    document.getElementById("gated-content").classList.remove("hidden");

    const accessToken = await auth0Client.getTokenSilently();

    document.getElementById("ipt-access-token").innerHTML = accessToken;
    const message = {
      type: "auth0_token",
      token: accessToken,
    };
    window.postMessage(message, "*");

    document.getElementById("ipt-user-profile").textContent = JSON.stringify(
      await auth0Client.getUser()
    );
  } else {
    document.getElementById("gated-content").classList.add("hidden");
  }
};

const login = async () => {
  await auth0Client.loginWithRedirect({
    authorizationParams: {
      redirect_uri: window.location.origin,
    },
  });
};

const logout = () => {
  auth0Client.logout({
    logoutParams: {
      returnTo: window.location.origin,
    },
  });
};

Hey there @arshsharma461 welcome to the community!

I have a hunch this is due to the lack of an audience param in access tokens. You’ll want to add it in your loginWithRedirect method:

const login = async () => {
  await auth0Client.loginWithRedirect({
    authorizationParams: {
      redirect_uri: window.location.origin,
      audience: "https://your-api-identifier"
    },
  });
};

The api identifier being that of the API you’ve registered in your dashboard:

1 Like

Thanks for the reply @tyf!

I implemented your suggestion: (in app.js)

const login = async () => {
  await auth0Client.loginWithRedirect({
    authorizationParams: {
      redirect_uri: window.location.origin,
      audience: "http://localhost",
    },
  });
};

But I still see this error in my console:

Application running on port 3000
InvalidTokenError: Invalid Compact JWS
    at /Users/arsh/code/authdemo/node_modules/express-oauth2-jwt-bearer/dist/index.js:300:19
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /Users/arsh/code/authdemo/node_modules/express-oauth2-jwt-bearer/dist/index.js:403:24

Any idea how I could fix/debug this?

1 Like

After your suggesting I even tried adding the audience field in other places in app.js like:

const configureClient = async () => {
  const response = await fetchAuthConfig();
  const config = await response.json();

  auth0Client = await auth0.createAuth0Client({
    domain: config.domain,
    clientId: config.clientId,
    audience: "http://localhost",
  });
};

and

    const accessToken = await auth0Client.getTokenSilently({
      audience: "http://localhost",
    });

and

const login = async () => {
  await auth0Client.loginWithRedirect({
    authorizationParams: {
      redirect_uri: window.location.origin,
      audience: "http://localhost",
    },
  });
};

But still it’s not working. My tenant id is dev-csc8n75wcojtwxmv in case you need to take a look. Thanks!

1 Like

Anoher update:

I used the Authentication API Debugger extension and got a token from there after adding http://localhost to the Audience list. When I use this token it works.

This means the code we’re writing to pass the audience is not working correctly? For reference I’m importing auth0 using:

    <script src="https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js"></script>

in the index.htm which the server serves. I made the code changes you suggested, did npm run dev, logged out from the webpage, logged in to get a new access token which should have the audience field set, and then make the request which fails.

When I decode the token I get using (https://jwt.io/) I see this:

This proves the token isn’t of the correct type but I’m not sure why that’s happening, can you please help out?

1 Like

Hey there @arshsharma461 thanks for the additional details, and happy to help!

It looks like you are still receiving an opaque token :thinking: Perhaps the use of http://localhost as an API identifier is giving you issues. Can you try registering a new API in your dashboard and using something like https://test-api-endpoint instead?

Thanks! Just tried it and even that isn’t working, in app.js:

const updateUI = async () => {
  const isAuthenticated = await auth0Client.isAuthenticated();

  document.getElementById("btn-logout").disabled = !isAuthenticated;
  document.getElementById("btn-login").disabled = isAuthenticated;

  if (isAuthenticated) {
    document.getElementById("gated-content").classList.remove("hidden");

    const accessToken = await auth0Client.getTokenSilently({
      audience: "https://textella-api-endpoint",
    });

    document.getElementById("ipt-access-token").innerHTML = accessToken;
    const message = {
      type: "auth0_token",
      token: accessToken,
    };
    window.postMessage(message, "*");

    document.getElementById("ipt-user-profile").textContent = JSON.stringify(
      await auth0Client.getUser()
    );
  } else {
    document.getElementById("gated-content").classList.add("hidden");
  }
};

and

const login = async () => {
  await auth0Client.loginWithRedirect({
    authorizationParams: {
      redirect_uri: window.location.origin,
      audience: "https://textella-api-endpoint",
    },
  });
};

Hey! I managed to finally get it working. I think what did the trick was specifying audience under authorizationParams instead of directly like earlier. It was needed only in the updateUI() function and not the login one.

What worked:

const updateUI = async () => {
// code

    const accessToken = await auth0Client.getTokenSilently({
      authorizationParams: {
        audience: "https://textella-api-endpoint",
      },
    });

// code
};

what wasn’t working earlier:

const updateUI = async () => {
// code

    const accessToken = await auth0Client.getTokenSilently({
        audience: "https://textella-api-endpoint",
    });

// code

Thanks for all the support!

1 Like

I had a follow up question, if you can help with that please. How do I get user details once I do have this validated token in my server.js:

const checkJwt = auth({
  audience: "https://textella-api-endpoint",
  issuerBaseURL: "https://dev-csc8n75wcojtwxmv.us.auth0.com/",
  tokenSigningAlg: "RS256",
});

app.post("/generate", checkJwt, async (req, res) => {
  const token =
    req.headers.authorization && req.headers.authorization.split(" ")[1];
  console.log(token);
  // get/validate user details from token
  // app code
});

In the api endpoint for /generate I want to get the details of the user that just got validated, how do I do that?

Thanks!

Hey @tyf! I just noticed that the code I had is causing a weird bug. Ever since I did this in app.js:

if (isAuthenticated) {
    document.getElementById("gated-content").classList.remove("hidden");

    const accessToken = await auth0Client.getTokenSilently({
      authorizationParams: {
        audience: "https://textella-api-endpoint",
      },
    });

    document.getElementById("ipt-access-token").innerHTML = accessToken;
    const message = {
      type: "auth0_token",
      token: accessToken,
    };
    window.postMessage(message, "*");

    document.getElementById("ipt-user-profile").textContent = JSON.stringify(
      await auth0Client.getUser()
    );
  } 

When I log in from a different user, it’s not able to get the the authToken and display anything in the HTML. But when I remove audience: "https://textella-api-endpoint", everything works as it did in the sample SPA JavaScript app. Any idea what might be causing this?

The error I get in my console is: Error: Consent required

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