Skip to content

fix(auth): honor APIFY_TOKEN env var over stored login token - #1293

Open
l2ysho wants to merge 6 commits into
masterfrom
claude/apify-token-permissions-bug-15c25d
Open

fix(auth): honor APIFY_TOKEN env var over stored login token#1293
l2ysho wants to merge 6 commits into
masterfrom
claude/apify-token-permissions-bug-15c25d

Conversation

@l2ysho

@l2ysho l2ysho commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What

Makes the APIFY_TOKEN environment variable take precedence over the token stored by apify login. It was being silently ignored, so:

  • APIFY_TOKEN=<customer_token> apify run failed with ApifyApiError: Insufficient permissions for the Actor run.
  • APIFY_TOKEN=<customer_token> apify actors ls returned Actors from the logged-in account, not the token's account.

Why

Two independent causes produced the same symptom:

  1. resolveToken (src/lib/utils.ts) — the single choke point behind getLoggedClient / getApifyClientOptions (~50 commands) resolved only an explicitly-passed token, then fell straight through to the stored token. It never consulted process.env.APIFY_TOKEN. This gap is longstanding (predates the keyring refactor, which faithfully preserved it).

  2. apify run (src/commands/run.ts) — the stored token was injected into the child env after process.env ({ ...process.env, ...localEnvVars }), overwriting an inherited APIFY_TOKEN. The merge order was intentionally flipped in fix: prevent modifying local input.json file #1042 so the CLI's input-key redirect vars win over inherited ones; that flip also caught the token as collateral damage.

Change

Precedence is now, everywhere: explicit --token arg → APIFY_TOKEN env var → stored login token.

  • resolveToken returns process.env.APIFY_TOKEN before falling back to the stored token.
  • apify run only injects the stored token when APIFY_TOKEN isn't already inherited — leaving fix: prevent modifying local input.json file #1042's input-key redirect behavior fully intact.

Tests

  • Local unit tests (test/local/lib/credentials.test.ts) — precedence at getApifyClientOptions: env var wins over stored, explicit arg wins over env var, stored token used when neither is set.
  • [api] regression test (test/local/commands/run.test.ts) — sets an inherited APIFY_TOKEN distinct from the stored token and asserts the spawned Actor sees the inherited one.

Reviewer notes

🤖 Generated with Claude Code

`APIFY_TOKEN=<token> apify run` and other commands silently ignored the
env var and used the token from `apify login` instead, so runs failed with
"Insufficient permissions" and `apify actors ls` returned the wrong account.

Two independent causes, same symptom:

- `resolveToken` (src/lib/utils.ts) resolved only an explicit token arg then
  the stored token, never `process.env.APIFY_TOKEN`. This choke point feeds
  ~50 commands via getLoggedClient/getApifyClientOptions. Precedence is now:
  explicit `--token` > `APIFY_TOKEN` > stored login.

- `apify run` injected the stored token into the child env after `process.env`
  (`{ ...process.env, ...localEnvVars }`), clobbering an inherited APIFY_TOKEN.
  The merge order was flipped in #1042 so the input-key redirect vars win; that
  flip also caught the token as collateral. Now the stored token only fills in
  when APIFY_TOKEN isn't already inherited, leaving #1042's behavior intact.

Adds local unit tests for the precedence at getApifyClientOptions and an
[api] regression test proving `apify run` doesn't override an inherited token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@l2ysho
l2ysho requested a review from DaveHanns as a code owner July 21, 2026 14:01
@github-actions github-actions Bot added this to the 145th sprint - Tooling team milestone Jul 21, 2026
@github-actions github-actions Bot added t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics. labels Jul 21, 2026
@l2ysho l2ysho added adhoc Ad-hoc unplanned task added during the sprint. t-builders Issues owned by the Builders team. and removed t-tooling Issues with this label are in the ownership of the tooling team. labels Jul 21, 2026
Comment thread src/lib/utils.ts

const resolveToken = async (existingToken?: string): Promise<string | undefined> => {
if (existingToken) return existingToken;
if (process.env[APIFY_ENV_VARS.TOKEN]) return process.env[APIFY_ENV_VARS.TOKEN];

@DaveHanns DaveHanns Jul 29, 2026

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.

APIFY_TOKEN now silently overwrites the stored apify login credentials

The read-side precedence added here is correct, but this line has a downstream side effect that I think makes it a blocker.

Because resolveToken() now returns process.env.APIFY_TOKEN, getLoggedClient(), reached by ~all authenticated commands via getLoggedClientOrThrow(), then persists it:

// src/lib/utils.ts, getLoggedClient()
const resolvedToken = await resolveToken(token);          // ← now the APIFY_TOKEN value

if (apifyClient.token) {
    await setToken(apifyClient.token, { skipIfUnchanged: true });   // ← writes it to keyring/auth.json
}

writeFileSync(AUTH_FILE_PATH(), JSON.stringify({ ...existingFile, ...userInfo }));  // ← rewrites username/id too

setToken(…, { skipIfUnchanged: true }) writes whenever the value differs from what's stored. Pre-PR, resolvedToken was always the stored token, so this was a no-op. Now:

apify login --token <A>   # stored login = account A
export APIFY_TOKEN=<B>    # a different, valid token
apify actors ls           # reads B (correct) — but setToken overwrites stored A with B,
                          #   and auth.json username/id are rewritten to account B
unset APIFY_TOKEN
apify actors ls           # resolveToken → getToken() → returns B  ← login A is gone

So a transient env var permanently mutates the durable login: the apify login account is silently replaced, and the change persists after unset. Two consequences:

  1. The env and stored tiers stop being independent — using APIFY_TOKEN destroys the stored login. This also contradicts the intent of the new run.ts guard's own comment ("must win over the stored login", i.e. without replacing it).
  2. It re-triggers the macOS Keychain write prompt that skipIfUnchanged was added to avoid — every command run with a differing APIFY_TOKEN rewrites the keyring.

Suggested fix: in getLoggedClient, only call setToken / setProxyPassword when the token came from an explicit apify login, not when it originated from APIFY_TOKEN or --token flag of command other than apify login (the --token on other commands should be one-time overwrite as well), mirroring the guard already added in run.ts:332.

(Heads-up: if you make this change, apify auth token will then print the stored token while other commands use the env token — it reads getLocalUserInfo().token directly and only looks correct today because of this overwrite. It'd need to become env-aware too.)

🤖 Generated with Claude Code

@l2ysho l2ysho Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

you, this is a good catch and I am wondering how I missed this when I self reviewed 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

also I am inspecting getLoggedClient() and there are few things I really do not like (for example it is doing mutations but name suggests it is only get), I will create a follow up issue if I find it is worth of it.

@DaveHanns

Copy link
Copy Markdown
Contributor

BTW, noticed the stale name of getApifyTokenFromEnvOrAuthFile. It no longer gets the token just from Auth file, but by default from keyring. We should simplify and correct the name.

@DaveHanns DaveHanns left a comment

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.

One possible blocker, otherwise looks good 🚀

l2ysho and others added 4 commits July 29, 2026 13:20
Addresses review on #1293: making resolveToken() return APIFY_TOKEN meant
getLoggedClient() then persisted it, because every authenticated command
reaches setToken(..., { skipIfUnchanged: true }) and the auth.json rewrite.
A transient env var permanently replaced the `apify login` account (and
re-triggered the macOS Keychain prompt skipIfUnchanged exists to avoid).

- getLoggedClient() takes `persistCredentials` (default false) and returns
  early before writing secrets or user metadata. Only `apify login` opts in,
  so --token / APIFY_TOKEN stay one-time overrides. The auth.json write is
  gated too, not just the secrets: userInfo carries username/id, so an
  override would otherwise swap the stored login's identity.
- `apify auth token` printed getLocalUserInfo().token, which only looked
  right because of the overwrite. It now resolves the same way the other
  commands authenticate, so it reports the token actually in use.
- Rename getApifyTokenFromEnvOrAuthFile -> getApifyToken: the name was stale
  (secrets come from the keyring by default now), and its env->stored chain
  duplicated resolveToken(), which it now delegates to.

Tests: getLoggedClient() leaves the stored token and username/id intact for
both APIFY_TOKEN and an explicit token, and persists only with
persistCredentials. apify-client is stubbed so this needs no API access.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ug-15c25d' into claude/apify-token-permissions-bug-15c25d

# Conflicts:
#	src/commands/auth/login.ts
`resolveToken` honors `APIFY_TOKEN`, but `username`/`id` still came from
auth.json — i.e. whoever ran `apify login`. Commands that build an Actor
name from the local identity (`builds create`, `actors push`, `task run`,
all storage commands, ...) therefore authenticated as one account and
resolved names against another, and in CI with no stored login at all they
sent the literal string "undefined/<actor>" to the API.

`getLocalUserInfo` now reads the identity from `user('me')` whenever
`APIFY_TOKEN` is set. `getLoggedClient` seeds a token-keyed cache with the
user info it already fetches, so commands that get a client first pay no
extra API call.

This also gives `apify run` the override account's proxy password and user
id, instead of pairing the inherited token with the stored account's
credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@l2ysho

l2ysho commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@vladfrangu I really want to know what is your opinion about resolveToken function. There are multiple scenarios (APIFY_TOKEN, --token, normal auth, tests) and when I fix one I break some other. On a first look It feelslike good candidate for a deep refactoring. WDYT?

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

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-builders Issues owned by the Builders team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants