fix(auth): honor APIFY_TOKEN env var over stored login token - #1293
fix(auth): honor APIFY_TOKEN env var over stored login token#1293l2ysho wants to merge 6 commits into
Conversation
`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>
|
|
||
| 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]; |
There was a problem hiding this comment.
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 toosetToken(…, { 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 goneSo a transient env var permanently mutates the durable login: the apify login account is silently replaced, and the change persists after unset. Two consequences:
- The env and stored tiers stop being independent — using
APIFY_TOKENdestroys the stored login. This also contradicts the intent of the newrun.tsguard's own comment ("must win over the stored login", i.e. without replacing it). - It re-triggers the macOS Keychain write prompt that
skipIfUnchangedwas added to avoid — every command run with a differingAPIFY_TOKENrewrites 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
There was a problem hiding this comment.
you, this is a good catch and I am wondering how I missed this when I self reviewed 🤔
There was a problem hiding this comment.
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.
|
BTW, noticed the stale name of |
DaveHanns
left a comment
There was a problem hiding this comment.
One possible blocker, otherwise looks good 🚀
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>
|
@vladfrangu I really want to know what is your opinion about |
What
Makes the
APIFY_TOKENenvironment variable take precedence over the token stored byapify login. It was being silently ignored, so:APIFY_TOKEN=<customer_token> apify runfailed withApifyApiError: Insufficient permissions for the Actor run.APIFY_TOKEN=<customer_token> apify actors lsreturned Actors from the logged-in account, not the token's account.Why
Two independent causes produced the same symptom:
resolveToken(src/lib/utils.ts) — the single choke point behindgetLoggedClient/getApifyClientOptions(~50 commands) resolved only an explicitly-passed token, then fell straight through to the stored token. It never consultedprocess.env.APIFY_TOKEN. This gap is longstanding (predates the keyring refactor, which faithfully preserved it).apify run(src/commands/run.ts) — the stored token was injected into the child env afterprocess.env({ ...process.env, ...localEnvVars }), overwriting an inheritedAPIFY_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
--tokenarg →APIFY_TOKENenv var → stored login token.resolveTokenreturnsprocess.env.APIFY_TOKENbefore falling back to the stored token.apify runonly injects the stored token whenAPIFY_TOKENisn't already inherited — leaving fix: prevent modifying local input.json file #1042's input-key redirect behavior fully intact.Tests
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 inheritedAPIFY_TOKENdistinct from the stored token and asserts the spawned Actor sees the inherited one.Reviewer notes
apify runregression traces specifically to fix: prevent modifying local input.json file #1042 (2026-04-28).apify runstill sourcesproxy/userIdfrom the stored account; only the token honors the env override. Out of scope for this fix (the reported bug is token-only).🤖 Generated with Claude Code