How can Offline Session tokens be generated for new apps using the latest Shopify App Remix and Token Exchange Process

I am using the latest Shopify App Remix package but I have not been able to generate an offline token using the new embedded app authorization strategy.

Here’s what I’m currently doing:

  1. Users installs the app
  2. When the app is loaded and rendered (embed) I request some data using a form, which ends up calling an API.
  3. The API calls the authenticate.admin(request) in order to generate a session token.

I notice that the accessToken generated always starts with shpua_, so I guess it’s not an offline access token.

I have tried using the oauth/access_token endpoint to generate an offline token but I always end up getting the following error:

Oauth error invalid_subject_token: Token exchange cannot be performed due to an invalid subject.

I call that endpoint with the following parameters:

{
“client_id”: “”,
“client_secret”: “”,
“grant_type”: “urn:ietf:params:oauth:grant-type:token-exchange”,
“subject_token”: “shpua_”, // Token generated from authenticate.admin()
“subject_token_type”: “urn:ietf:params:oauth:token-type:id_token”,
“requested_token_type”: “urn:shopify:params:oauth:token-type:offline-access-token”
}

Any ideas on what I might be doing wrong?

The subject token here is the users session token not an access token. The docs have a good example to follow

Thank you for the reply! I am following that guide but it’s still not very clear to me which value I should send to the oauth/access_token endpoint.

At the moment, I am sending this:

// Here's how I authenticate the request and get a session token
const { admin, session, sessionToken } = await authenticate.admin(request);

{
    "client_id": "",
    "client_secret": "",
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "subject_token": "shpua_53......", // This is the token retrieved using session.accessToken
    "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
    "requested_token_type": "urn:shopify:params:oauth:token-type:offline-access-token"
}

When sending that, I get the following error:

Oauth error invalid_subject_token: Token exchange cannot be performed due to an invalid subject token.

And I can’t send the sessionToken because it is a JSON object, not a string.

Oh rereading this the authenticate.request method will return you an offline access token anyway Admin

As part of the session object. So you can just use that

I have also tried that, but when I call an API using the Ruby library or directly from Postman, if I include the token retrieved from admin.authenticated(request) I always end up getting this error:

{"errors":"[API] Invalid API key or access token (unrecognized login or wrong password)"}

That token starts with shpua_ so maybe it’s not an offline token at all :confused: weird stuff.

Are you using managed installs?
What have you configured in your Shopify app Toml or config when creating the Shopify object

Yes, I am using managed installs. Here’s what I have when I initiate the shopify app

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY,
  apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
  apiVersion: ApiVersion.January25,
  scopes: process.env.SCOPES?.split(","),
  appUrl: process.env.SHOPIFY_APP_URL || "",
  authPathPrefix: "/auth",
  sessionStorage: new PrismaSessionStorage(prisma),
  distribution: AppDistribution.AppStore,
  useOnlineTokens: false,
  future: {
    unstable_newEmbeddedAuthStrategy: true
  },
  ...(process.env.SHOP_CUSTOM_DOMAIN
    ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
    : {}),
});

export default shopify;

Here’s my shopify.app.toml file

# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration

client_id = ""
application_url = ""
embedded = true
name = ""
handle = ""

[webhooks]
api_version = "2025-01"

  [[webhooks.subscriptions]]
  topics = [ "app/uninstalled" ]
  uri = "/webhooks/app/uninstalled"

  [[webhooks.subscriptions]]
  topics = [ "app/scopes_update" ]
  uri = "/webhooks/app/scopes_update"

[access.admin]
direct_api_mode = "offline"
embedded_app_direct_api_access = true

[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_inventory,read_locations,read_products,read_product_listings,write_products,write_inventory"
use_legacy_install_flow = false

[auth]
redirect_urls = [
]

[pos]
embedded = false

[build]
dev_store_url = "lion-backups.myshopify.com"
automatically_update_urls_on_dev = true
include_config_on_deploy = true

I followed this link to set up the configuration.