Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

InvalidJwtError: Failed to parse session token when using Shopify.Utils.loadCurrentSession #137

Closed
1 task done
znapfel opened this issue Mar 15, 2021 · 21 comments
Closed
1 task done

Comments

@znapfel
Copy link

znapfel commented Mar 15, 2021

Issue summary

Hi there,

Thanks for making this node API!

I've been making a test development app to learn more about how cookieless sessions should work in Shopify apps.

However, Shopify.Utils.loadCurrentSession is suddenly failing to parse session tokens provided by making a request to API routes using authenticatedFetch from @shopify/app-bridge-utils. This was working late last week, but when I loaded my development app today, this suddenly started failing with no changes on my part.

InvalidJwtError: Failed to parse session token 'base64 string here': jwt not active    at InvalidJwtError.ShopifyError [as constructor] (\node_modules\@shopify\shopify-api\dist\error.js:13:28)

Expected behavior

authenticatedFetch should request the data from my test route and attach the Authorization header, which is then parsed within the Shopify object on the back end and return a session, which I can check the presence of, and complete the API request with dummy data.

Actual behavior

The session now fails to load in the /test route, due to a failure to parse the provided token that the authenticatedFetch is attaching to the request. The base 64 string matches in the backend error logs and on the front end network request, though:

Frontend request

Authorization | Bearer   eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczpcL1wvcHJvZHVjdC1vcHRpb25zLXRlc3QxNS5teXNob3BpZnkuY29tXC9hZG1pbiIsImRlc3QiOiJodHRwczpcL1wvcHJvZHVjdC1vcHRpb25zLXRlc3QxNS5teXNob3BpZnkuY29tIiwiYXVkIjoiODUxOWFmNzMzZTRhNThlNDBlNDlkZjhmMWVkM2U4YTAiLCJzdWIiOiIzNjA0MzQ4OTMyOCIsImV4cCI6MTYxNTgzMTAwMSwibmJmIjoxNjE1ODMwOTQxLCJpYXQiOjE2MTU4MzA5NDEsImp0aSI6IjVkZjRmODgzLTY5YWMtNGY3ZC05ZjhkLWNlYzQ2YWIwYjg3OSIsInNpZCI6ImI3YTM5ODg1MjRhNzI0NDgyZjYyMWNjODg2ZDI1M2ExOGQ5NjcyMmZkMjhmNzY2ZGZhNTEwMzQxM2FhZmVjNDUifQ.O7E8xBPyzskfomLuGwz_0BjnpY6Az6vqk9mDjHXbJTQ

server error

InvalidJwtError: Failed to parse session token 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczpcL1wvcHJvZHVjdC1vcHRpb25zLXRlc3QxNS5teXNob3BpZnkuY29tXC9hZG1pbiIsImRlc3QiOiJodHRwczpcL1wvcHJvZHVjdC1vcHRpb25zLXRlc3QxNS5teXNob3BpZnkuY29tIiwiYXVkIjoiODUxOWFmNzMzZTRhNThlNDBlNDlkZjhmMWVkM2U4YTAiLCJzdWIiOiIzNjA0MzQ4OTMyOCIsImV4cCI6MTYxNTgzMTAwMSwibmJmIjoxNjE1ODMwOTQxLCJpYXQiOjE2MTU4MzA5NDEsImp0aSI6IjVkZjRmODgzLTY5YWMtNGY3ZC05ZjhkLWNlYzQ2YWIwYjg3OSIsInNpZCI6ImI3YTM5ODg1MjRhNzI0NDgyZjYyMWNjODg2ZDI1M2ExOGQ5NjcyMmZkMjhmNzY2ZGZhNTEwMzQxM2FhZmVjNDUifQ.O7E8xBPyzskfomLuGwz_0BjnpY6Az6vqk9mDjHXbJTQ': jwt not active    at InvalidJwtError.ShopifyError [as constructor]

What actually happens?

InvalidJwtError: Failed to parse session token 'base64 string here: jwt not active    at InvalidJwtError.ShopifyError [as constructor] (\node_modules\@shopify\shopify-api\dist\error.js:13:28)
    at new InvalidJwtError (\node_modules\@shopify\shopify-api\dist\error.js:39:42)
    at Object.decodeSessionToken (\node_modules\@shopify\shopify-api\dist\utils\decode-session-token.js:20:15)
    at Object.getCurrentSessionId (\node_modules\@shopify\shopify-api\dist\auth\oauth\oauth.js:218:64)
    at Object.<anonymous> (\node_modules\@shopify\shopify-api\dist\utils\load-current-session.js:19:46)
    at step (\node_modules\@shopify\shopify-api\node_modules\tslib\tslib.js:143:27)
    at Object.next (\node_modules\@shopify\shopify-api\node_modules\tslib\tslib.js:124:57)
    at \node_modules\@shopify\shopify-api\node_modules\tslib\tslib.js:117:75
    at new Promise (<anonymous>)
    at Object.__awaiter (\node_modules\@shopify\shopify-api\node_modules\tslib\tslib.js:113:16)
    at Object.loadCurrentSession (\node_modules\@shopify\shopify-api\dist\utils\load-current-session.js:15:20)

Steps to reproduce the problem

  1. Install Development app into dev store and authenticate
  2. When index.js loads, getTestData will make the request to the api route specified in server.js
  3. Session token will fail parsing from inside Shopify.Utils.loadCurrentSession. This was working on Friday with the below reduced test case, but now it's not.

Reduced test case

server.js

require("isomorphic-fetch");
const dotenv = require("dotenv");
dotenv.config();
const Koa = require("koa");
const next = require("next");
const { default: createShopifyAuth } = require("@shopify/koa-shopify-auth");
const { verifyRequest } = require("@shopify/koa-shopify-auth");
const { default: Shopify, ApiVersion } = require("@shopify/shopify-api");
const Router = require("@koa/router");

const { SHOPIFY_APP_SECRET, SHOPIFY_APP_KEY } = process.env;

Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_APP_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_APP_SECRET,
  SCOPES: ["read_products", "write_products"],
  HOST_NAME: process.env.BASE_URL.replace(/https:\/\//, ""),
  API_VERSION: ApiVersion.January21,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

const ACTIVE_SHOPIFY_SHOPS = {};

app.prepare().then(() => {
  const server = new Koa();
  const router = new Router();

  server.keys = [Shopify.Context.API_SECRET_KEY];

  server.use(
    createShopifyAuth({
      accessMode: "offline",
      async afterAuth(ctx) {
        const { shop, scope, accessToken } = ctx.state.shopify;
        console.log({ shopify: ctx.state.shopify });
        ACTIVE_SHOPIFY_SHOPS[shop] = scope;
        console.log({ ACTIVE_SHOPIFY_SHOPS });

        ctx.redirect(`/?shop=${shop}`);     
      },
    })
   
  );

  const handleRequest = async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  };

  router.get("/", async (ctx) => {
    const shop = ctx.query.shop;

    if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
  });
  
  router.get("/test", async (ctx) => {
    console.log("before load session");
    const session = await Shopify.Utils.loadCurrentSession(
      ctx.request,
      ctx.response,
      false
    );
    console.log({ session });
  
    return (ctx.body = { it: "worked" });
  });


  router.get("(/_next/static/.*)", handleRequest);
  router.get("/_next/webpack-hmr", handleRequest);
  router.get("/__nextjs_original-stack-frame", handleRequest);
  router.get("(.*)", verifyRequest(), handleRequest);

  server.use(router.allowedMethods());
  server.use(router.routes());

  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

index.js - Main Page of React App

import { authenticatedFetch } from "@shopify/app-bridge-utils";

  async componentDidMount() {
    this.getTestData();
  }

  getTestData = async () => {
    const app = this.context;
    const response = await authenticatedFetch(app)(
      `${APP_URL}/test`
    ).then(
      (result) => {
        console.log({ result });
      },
      (error) => {
        this.setState({ error });
      }
    );
  };

Checklist

  • I have described this issue in a way that is actionable (if possible)
@zJunYao
Copy link

zJunYao commented Mar 16, 2021

I have the same question .

@paulomarg
Copy link
Contributor

Hey @znapfel, the jwt not active error is actually thrown by the jsonwebtoken library itself: https://github.com/auth0/node-jsonwebtoken#notbeforeerror. It seems that the nbf field is still in the future by the time the request reaches the server.

This is quite curious, I wonder if it happens because your request is happening too quickly after loading the app, so that the token isn't active yet. Just so we can rule out an issue on app bridge, could you please try a couple of different things?

  • Call await getSessionToken(app) as soon as the app loads (it can be called right after createApp)
  • That failing, could you double check what value you're getting for nbf on the server side? You can use Shopify.Utils.decodeSessionToken to parse the JWT before calling loadCurrentSession.

Let me know how that goes and we can raise the issue to App Bridge if we're getting an invalid value.

@karmapandya
Copy link

@paulomarg Yeah. that's what it seems like. It's happening because the token isn't active yet. I am using a workaround by setTimeout (not ideal), but that seems to work fine for now.

@VladimirCatrici
Copy link

VladimirCatrici commented Apr 7, 2021

In my case, the nbf was 3 seconds ahead of the time on my machine. Triggering time sync manually helped.

@ilugobayo
Copy link

ilugobayo commented Apr 9, 2021

@paulomarg is there a way to use loadCurrentSession in the "/" endpoint?

I have this issue, I don't know if any of you have dealt with it, everything works fine when I install/load the app, even on incognito; I'm using the same approach as @znapfel to request data needed in the frontend from the backend, the only difference is that I'm using verifyRequest() on my endpoints:

route in server.js

router.get('/api/bayonet/get-paid-status', verifyRequest(), async (ctx) => {
    const sessionToken = (ctx.headers.authorization).replace('Bearer ', '');
    var payloadToken = jwt.verify(sessionToken, Shopify.Context.API_SECRET_KEY, { algorithms: ['HS256'] });
    const shopDomain = payloadToken['dest'].substring(payloadToken['dest'].indexOf('https://') + 8, payloadToken['dest'].length);

    try {
      const paidStatus = await dbGetHelper.getPaidStatus(shopDomain);

      if (paidStatus !== undefined && paidStatus !== null) {
        ctx.body = {
          data: paidStatus
        };
      } else {
        ctx.body = {
          data: null
        };
        logger.error(stringInject.default(logMessages.serverPaidStatusUndefined, [shopDomain]));
      }
    } catch (err) {
      ctx.body = {
        data: null
      };
      logger.error(stringInject.default(logMessages.serverGetPaidStatusCatch, [shopDomain, err]))
    }
  });

function to perform requests in a page, in this case index.js

makeGetRequest = async (endpoint) => {
    const app = this.context;
    const result = await authenticatedFetch(app)(endpoint,
      {
        method: 'GET',
        headers: {
          'Accept': 'application/json',
        },
      })
      .then(resp => resp.json())
      .then(json => {
        return json;
      });

    return result;
  };

But whenever I log out from the admin dashboard and log in again, a new session is generated but is never updated on my app; this causes a very weird issue where my custom endpoint will accept the request from the frontend as I'm sending a session token (which has a 1 minute expiration time) in the authorization header and using the verifyRequest() function but some of these requests include retrieving data from the Shopify API, which requires an accessToken and since the session was never updated, the accessToken stored in my database isn't valid any longer.

I decided to debug the app and noticed that in that scenario, when loading the app and hitting the "/" endpoint, ctx.query has a different session value than the one stored in my database:

Screenshot from 2021-04-09 04-45-48

Screenshot from 2021-04-09 04-58-11

This is why I was trying to load the current session and compare the session values, to then redirect to the auth process if they didn't matched but the loadCurrentSession gives me undefined on the "/" endpoint, though the session loads fine on any other of my endpoints, the only difference is the use of verifyRequest().

Oddly enough, when I directly hit any of my other pages that are not index.js ("/"), this is entering the URL in the address bar instead of pressing the navigation buttons in the embedded app, then it redirects the user to the auth process and then, the session token is updated in my database, along with the accessToken; I think this is because those URLs fall in router.get("(.*)", verifyRequest(), handleRequest); but then, shouldn't that happen with my custom endpoints as well since I'm using verifyRequest()?

It's not clear to me what's the difference between the session token that we store using the CustomSessionStorage (24 hours of expiration time) and the session token sent in the requests to the backend using authenticatedFetch (1 minute of expiration time).

Any help will be highly appreciated, thanks.

@znapfel
Copy link
Author

znapfel commented Apr 10, 2021

It looks like my clock was no longer synced after a Windows 10 update I applied between when loadCurrentSession was originally working and when it stopped.

Manually syncing the clock fixed the issue with nbf being off, and everything was working fine.

Thanks @paulomarg and @VladimirCatrici!

@paulomarg
Copy link
Contributor

Glad to hear that @znapfel, I'll go ahead and close this issue then since it wasn't a problem with the library code itself.

@ltakens
Copy link

ltakens commented Apr 14, 2021

On my machine that syncs with time.apple.com nbf can be about 1-2 seconds ahead. Perhaps a clockTolerance at this line would be an idea if many app developers experience this issue

@giladv
Copy link

giladv commented Apr 15, 2021

guys, this is also happening to me. seems like only on local environment. requests made right after starting the server are working but after a few mins they fail with the jwt error

@karmapandya
Copy link

karmapandya commented Apr 15, 2021

@giladv You can try settimeout or recursive function on local environment (If time sync isn't working).

This is just a suggestion for dev environment.

Try this approach.

  1. You can create a helper function that parses current session with loadCurrentSession.
  2. call helper function when request is made to your route to parse the session.
  3. if jwt error message == not active => call helperfunction again(recursively).
  4. else parse token and return session.
  5. try-catch other errors and return result accordingly.

Pseudocode. (I haven't tested this yet)

const helperFunc = async (ctx) => {
try {
//parse session here.
}
catch(err){
// handle error
if(err === "jwt not active" {
helperFunc (ctx)
}
else{
return null
}
}
}

Again, this is hacky way to handle it but it should work for a testing environment. You might need to improve/change it for production.

@giladv
Copy link

giladv commented Apr 15, 2021

thanks @karmapandya, but the error for me is originated from verifyRequest() middleware which responds to the request on its own. so my solution was this

const verifyRequestMiddleware = verifyRequest();

router.all('(.*)', async (ctx, next) => {
	  await sleep(3000);
	  await verifyRequestMiddleware(ctx, next)
  }, handleRequest); 

but this is not not something we can actually use long term, not even for development.
there is a CRITICAL bug going on somewhere and i'm wondering - are shopify people on it?

@rashedxyz
Copy link

Facing the same problem from windows 10.

@mattlockyer
Copy link

It looks like my clock was no longer synced after a Windows 10 update I applied between when loadCurrentSession was originally working and when it stopped.

Can confirm this on Ubuntu. Settings -> turn on/off automatic date/time and then app worked fine.

@WeikunYe
Copy link

Confirmed that sync system clock on Win10 works. If you are using the Win10 system for development:

  • Open Settings
  • Click Time & Language
  • Click Sync Now button
    Then, restart your app and the session should be loaded.

@codingphasedotcom
Copy link

This is crazy I wasted so much time and it was because my computer was not in sync... things like this humbles any developer

@daviareias
Copy link

I'm still getting this error every 2 to 3 days in my dockerized app

@pooya-dehghan
Copy link

solved the problem with sync button in windows 10 (settings time&date )

@ELBEQQAL94
Copy link

The same proble with me, The solution is to switch from manualy local time to auto timing on my machine.

OS: ubuntu

@oribenez
Copy link

I have this issue also on heroku on production

@dgtlmonk
Copy link

dgtlmonk commented Nov 4, 2022

have this issue on fly.io

@mirceapiturca
Copy link

I'm constantly getting these on Heroku. It crashes the app:

Error Failed to parse session token 'eyJhb...': jwt expired 
    node_modules/@shopify/shopify-api/dist/error.js:13:28 InvalidJwtError.ShopifyError [as constructor]
    node_modules/@shopify/shopify-api/dist/error.js:47:42 new InvalidJwtError
    node_modules/@shopify/shopify-api/dist/utils/decode-session-token.js:23:15 decodeSessionToken
    node_modules/@shopify/shopify-api/dist/auth/oauth/oauth.js:174:69 Object.getCurrentSessionId
    node_modules/@shopify/shopify-api/dist/utils/load-current-session.js:19:46 Object.<anonymous>
    node_modules/tslib/tslib.js:144:27 step
    node_modules/tslib/tslib.js:125:57 Object.next
    node_modules/tslib/tslib.js:118:75 
    node_modules/tslib/tslib.js:114:16 Object.__awaiter

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests