Skip to content

Name the missing access when a token exchange is refused - #9000

Open
egaodd wants to merge 2 commits into
mainfrom
river/automation-token-exchange-errors
Open

egaodd wants to merge 2 commits into
mainfrom
river/automation-token-exchange-errors

Conversation

@egaodd

@egaodd egaodd commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?
Found in the service accounts bug bash (P2): a dev-stores-only service account token running shopify app versions list was told "The custom token provided can't be used for the App Management API. Ensure the token is correct and not expired." The token was valid; it just has no app access. A revoked token got exactly the same message, so you couldn't tell which problem you had.

WHAT is this pull request doing?
Identity already says which it is in the exchange response's error_description: Invalid 'scope' value: new scopes must not be specified (or no useable scope) when the token doesn't hold a scope the exchange asked for, and Invalid 'subject_token' value: … when the token itself is refused (revoked, expired, malformed, unknown). The CLI logged that in --verbose output and then threw it away.

tokenRequest keeps the description and InvalidRequestError carries it.
The automation-token exchange picks its message from it:
missing scope: "The service account behind SHOPIFY_SERVICE_ACCOUNT_TOKEN doesn't have App development access", with the capability named the way the admin names it (App development / Dev store management) and where to grant it. A legacy SHOPIFY_APP_AUTOMATION_TOKEN / SHOPIFY_CLI_PARTNERS_TOKEN is pointed at a service account token instead.
refused token: "The token in was rejected: it has been revoked, has expired, or isn't a valid token."
anything else, including Identity's third 'scope' message (about the CLI's own request) and network failures, keeps today's wording.
Identity's own wording still only reaches the debug log.
How to test your changes?
Verified against production (org 299069):

A dev-stores-only service account token running shopify app versions list prints the "doesn't have App development access" banner; --verbose shows Identity's Invalid 'scope' value: new scopes must not be specified.
A made-up token running shopify store list prints the "was rejected: it has been revoked, has expired, or isn't a valid token" banner.
Unit tests in exchange.test.ts cover the scope description, the subject_token descriptions (400 and the 401 "inactive" variant), and the legacy-variable wording. tsc and eslint pass on cli-kit.

Measuring impact
n/a (bug fix)
Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • Changeset added (@shopify/cli-kit, @shopify/cli: patch)

A dev-stores-only service account token running `app versions list` was
told "The custom token provided can't be used for the App Management API.
Ensure the token is correct and not expired." The token was fine; it just
has no app access. A revoked token got the same message, so the person
couldn't tell which problem they had.

Identity says which it is in the exchange response's error_description:
"Invalid 'scope' value: ..." when the token doesn't hold a scope the
exchange asked for, "Invalid 'subject_token' value: ..." when the token
itself is refused (revoked, expired, malformed, unknown). The CLI logged
that description in verbose output and then threw the description away.

`tokenRequest` now keeps the description, `InvalidRequestError` carries
it, and the automation-token exchange reads it to pick a message:

- missing scope: "The service account behind SHOPIFY_SERVICE_ACCOUNT_TOKEN
  doesn't have App development access", with the capability named as the
  admin names it and where to grant it. A legacy automation token is
  pointed at service accounts instead.
- refused token: "The token in <variable> was rejected: it has been
  revoked, has expired, or isn't a valid token."
- anything else keeps today's wording.

Identity's own wording still only reaches the debug log.

Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
@egaodd
egaodd requested a review from a team as a code owner October 23, 2026 15:41
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Oct 23, 2026
'Use a service account token that has it: create one',
...admin,
'and set it in',
{command: environmentVariables.serviceAccountToken},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, AI-assisted review: Could we also tell users to unset the legacy variable when setting SHOPIFY_SERVICE_ACCOUNT_TOKEN? Otherwise, adding the new variable while leaving the old one set triggers the CLI's variable-conflict error and costs another failed attempt.

…ount variable

When a legacy automation token lacks the access an exchange asks for, the
advice is to create a service account token and set it in
SHOPIFY_SERVICE_ACCOUNT_TOKEN. Setting that beside the old variable is
refused as a conflict, so the advice now says to unset the old one too.
From Liz Kenyon's review.

Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
@github-actions

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/session/exchange.d.ts
@@ -4,6 +4,9 @@ import { Result } from '../../../public/node/result.js';
 export declare class InvalidGrantError extends Error {
 }
 export declare class InvalidRequestError extends Error {
+    /** Identity's `error_description`, when it sent one. Says what about the request was invalid. */
+    readonly description: string;
+    constructor(description?: string);
 }
 export interface ExchangeScopes {
     admin: string[];

@egaodd
egaodd enabled auto-merge October 23, 2026 17:25

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants