Token Mint Clock Skew Issue
I maintainer di solito rispondono entro 3 giorni
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 75/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- nodejs, typescript
- Ambito
- authentication, cli
Direzione di ricerca
Il problema si trova in src/credentials/ios/appstore/authenticate.ts, in particolare nella costante jwtDurationSeconds impostata a 1200. Esamina la funzione authenticateWithApiKeyAsync e il costruttore Token. La correzione consiste nel ridurre questa durata (ad esempio a 1140 secondi) per consentire lo skew dell'orologio. Testa modificando la costante ed eseguendo lo script di riproduzione fornito per verificare che il token venga accettato. Controlla se ci sono test relativi all'autenticazione Apple.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Build/Submit details page URL
No response
Summary
eas-cli mints its App Store Connect JWT with exp = iat + 1200, exactly Apple's twenty-minute
maximum. Apple requires exp to be strictly less than 1200 seconds ahead of its own clock, so
the token is valid only if the client's clock is behind Apple's. Any machine synchronised exactly,
or running even fractionally fast, gets HTTP 401 NOT_AUTHORIZED — and eas-cli then falls back to
interactive Apple ID login, which hides the cause.
Managed or bare?
Managed (CNG — no ios/ or android/ directories; configured via app.config.ts).
Environment
Environment
The reproduction below is a standalone script that never loads the Expo project.
Equivalent details, read from the installed tree:
- eas-cli 21.8.0 — also reproduced on 24.7.0;
jwtDurationSecondsis identical in both - @expo/apple-utils 2.1.22
- expo 54.0.33, react-native 0.81.5 (managed / CNG — no
ios/orandroid/directories) - Node v22.14.0, Yarn 4.5.0, npm 10.9.2
- OS Windows 11 (10.0.26200) x86_64
- Auth App Store Connect API key — Team key, App Manager role, supplied via
EXPO_ASC_API_KEY_PATH/EXPO_ASC_KEY_ID/EXPO_ASC_ISSUER_ID - Clock within 1 second of Apple's, measured from the
Dateresponse header on
api.appstoreconnect.apple.com
Error output
√ Select your Apple Team Type: » Company/Organization
√ Apple Team ID: ... XXXXXXXXXX
Auth error: 'Apple 401 detected - You are either not logged in, your account doesn't have
access to the requested data, or the page doesn't exist
Authentication credentials are missing or invalid. - Provide a properly configured and signed
bearer token, and make sure that it has not expired.'. Login in again (remaining: 3)
Log in to your Apple Developer account to continue
The fallback to interactive login is what obscures this: the visible failure is an Apple ID prompt,
not a refused token. Where interactive login is unavailable, the command simply dies.
Reproducible demo or steps to reproduce from a blank project
Important: whether this reproduces on your machine depends on your clock. If yours runs behind
Apple's — common, since NTP correction is biased slow by network latency — eas-cli works fine and
you will not see it. The script below therefore simulates the offset in the token itself, so it
reproduces deterministically regardless of your own clock.
The failing call is independent of eas-cli, so this needs only a valid ASC API key. It mints tokens
exactly as eas-cli does, varying clock offset and duration independently.
// repro.js — node >= 18
// usage: node repro.js <offsetSeconds> <durationSeconds> <runs>
// offset > 0 simulates a client clock running FAST by that many seconds
const crypto = require('node:crypto'), fs = require('node:fs')
const { EXPO_ASC_API_KEY_PATH: keyPath, EXPO_ASC_KEY_ID: keyId, EXPO_ASC_ISSUER_ID: issuerId } = process.env
const offset = Number(process.argv[2] ?? 0)
const duration = Number(process.argv[3] ?? 1200)
const runs = Number(process.argv[4] ?? 3)
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url')
const pem = fs.readFileSync(keyPath, 'utf8')
function mint() {
const iat = Math.floor(Date.now() / 1000) + offset
const h = b64({ alg: 'ES256', kid: keyId, typ: 'JWT' })
const p = b64({ iss: issuerId, iat, exp: iat + duration, aud: 'appstoreconnect-v1' })
const s = crypto.createSign('SHA256'); s.update(`${h}.${p}`)
return `${h}.${p}.${s.sign({ key: pem, dsaEncoding: 'ieee-p1363' }).toString('base64url')}`
}
;(async () => {
const out = []
for (let i = 0; i < runs; i++) {
const r = await fetch('https://api.appstoreconnect.apple.com/v1/bundleIds?limit=1', {
headers: { Authorization: `Bearer ${mint()}` },
})
out.push(r.status)
await new Promise((res) => setTimeout(res, 400))
}
console.log(`offset=+${offset}s duration=${duration}s -> ${out.join(' ')}`)
})()
Results. offset simulates clock lead; duration is what eas-cli controls:
node repro.js 0 1200 3 -> 401 401 401 # what eas-cli sends, clock ~0s from Apple
node repro.js 0 1200 3 -> 401 200 401 # same params, rerun — flaps on the boundary
node repro.js 0 1199 3 -> 200 200 200 # one second under the ceiling
node repro.js 0 1140 3 -> 200 200 200
node repro.js 5 1200 3 -> 401 401 401 # clock 5s fast, current behaviour
node repro.js 5 1140 3 -> 200 200 200 # clock 5s fast, with margin
node repro.js 60 1140 3 -> 401 401 401 # 60 + 1140 = 1200 exactly — rejected
node repro.js 60 1200 3 -> 401 401 401
Every row fits one rule: Apple accepts the token when duration + clock_offset < 1200, and rejects
it at or above. The offset=60 / duration=1140 row is the useful one — landing on exactly 1200 is
refused, which is why duration=1200 with a perfectly synchronised clock cannot work.
Source: src/credentials/ios/appstore/authenticate.ts, in authenticateWithApiKeyAsync:
const jwtDurationSeconds = 1200; // 20 minutes
passed as duration to new Token({ key, issuerId, keyId, duration }).
Choosing the exact maximum leaves zero tolerance for clock skew. The client must be at or
behind Apple's clock for authentication to work at all, which is why this presents as intermittent
and machine-specific rather than as a deterministic bug. In our case, correcting a one-second clock
lead with w32tm /resync made failures more consistent, not fewer — the opposite of what anyone
debugging this would expect.
Suggested fix: subtract a skew allowance rather than minting at the ceiling. Sixty seconds of
tolerance (duration = 1140) covers ordinary NTP drift; more would cover machines that are not
synchronised at all. We're running a local yarn patch at 1140 and the failures stopped completely.
Nineteen minutes appears to be established practice in other App Store Connect clients for exactly
this reason — see isaced/appstore-connect-sdk#25 and the fastlane discussions #18614 and #21275.
Possibly the same root cause: #2913, #2764, #2458, #2272, #1459 — all "Apple authentication
failed / invalid session" reports with no root cause established, which is how this presents once
the interactive-login fallback takes over.
- Lingua principale
- TypeScript
- Stelle
- 1.4k
- Fork
- 258
- Merge medio
- 3g 14h
- PR unite (30g)
- 133
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di expo/eas-cli
-
needs review
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 72/100
I maintainer di solito rispondono entro 3 giorni
-
eas update uses the legacy export command (and warns `--non-interactive is not supported`) when npm nests `@expo/cli` under `expo`Forse già presa @pcbeingused333 l’ha presa 5 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 3 giorni
-
ASC API key auth fails with Apple 401 because @expo/apple-utils signs JWTs with exactly the 1200 s maximum lifetimeForse già presa @ceydaakin l’ha presa 17 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 3 giorni
-
metadata:push: `apple.info.restrictedWords` flags "beta" inside other words (e.g. Danish "Betal")Forse già presa @DYNOSuprovo l’ha presa 30 giorni fa. Apertastale
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
expo/eas-cli#4501 · 1 commento ·
I maintainer di solito rispondono entro 3 giorni
-
workflow:validate crashes with "Cannot read properties of undefined (reading 'const')" when the published schema has a job entry without properties.typeForse di nuovo libera @douglowder l’ha presa 32 giorni fa e non c’è nessuna pull request aperta. Apertaissue accepted
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
expo/eas-cli#4482 · 3 commenti · 3 reazioni · 1 assegnatario ·
I maintainer di solito rispondono entro 3 giorni
Tutte le issue di expo/eas-cli
Issue simili
-
triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
🎙️ task - drop the raw `./node_modules/.bin/rhachet roles boot` line from bhuild's feedback templateAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
ehmpathy/rhachet-roles-bhuild#639 ·
I maintainer di solito rispondono entro 1 giorno
-
e2e-failure ready-to-code
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
redhat-developer/rhdh-plugin-export-overlays#4650 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
I maintainer di solito rispondono entro 13 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
omdsh-dev/DSH-better-sidebar#1098 ·
I maintainer di solito rispondono entro 2 giorni