{"record":{"id":"48fc5d24d1e7914b","repo":"jdx/mise","slug":"github-actions-oidc-token-expires-too-soon","errorCode":null,"errorMessage":"GitHub Actions OIDC token expires too soon","messagePattern":"GitHub Actions OIDC token expires too soon","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/lib.rs","lineNumber":749,"sourceCode":"                    .client\n                    .get(self.request_url.clone())\n                    .header(AUTHORIZATION, self.request_token.clone())\n                    .send()\n                    .await?\n                    .error_for_status()?\n                    .json()\n                    .await?)\n            })\n            .await\n            .map_err(|err| {\n                eyre!(\n                    \"failed to acquire GitHub Actions OIDC token for audience {:?}: {err}\",\n                    self.audience\n                )\n            })?;\n        let expires_at = jwt_expiry(&response.value)?;\n        if expires_at <= now.saturating_add(REFRESH_LEEWAY_SECONDS) {\n            bail!(\"GitHub Actions OIDC token expires too soon\");\n        }\n        let authorization = authorization_header(Some(&response.value))?\n            .ok_or_else(|| eyre!(\"GitHub Actions returned an empty OIDC token\"))?;\n        *cached = Some(CachedOidcToken {\n            authorization: authorization.clone(),\n            expires_at,\n        });\n        Ok(authorization)\n    }\n}\n\nfn jwt_expiry(token: &str) -> Result<u64> {\n    let payload = token\n        .split('.')\n        .nth(1)\n        .ok_or_else(|| eyre!(\"GitHub Actions returned a malformed OIDC token\"))?;\n    let payload = URL_SAFE_NO_PAD\n        .decode(payload)","sourceCodeStart":731,"sourceCodeEnd":767,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/crates/mise-cache-core/src/lib.rs#L731-L767","documentation":"When acquiring a GitHub Actions OIDC token, the client parses the JWT exp claim and refuses tokens expiring within 60 seconds (REFRESH_LEEWAY_SECONDS). This prevents sending a token that would already be invalid by the time the cache request lands. The same leeway governs when a cached token is refreshed.","triggerScenarios":"The runner's system clock running ahead of GitHub's time (skew larger than the token lifetime minus 60s); GitHub or a proxy returning a token whose exp is nearly immediate; a test or mock minting a JWT with exp in the past; a cached OIDC response replayed by an intermediary.","commonSituations":"Self-hosted runners without NTP synchronization; CI steps that acquire the credential and then stall; corporate proxies in front of ACTIONS_ID_TOKEN_REQUEST_URL that cache responses; unit tests whose fixture tokens have fixed exp timestamps.","solutions":["Synchronize the machine clock (enable NTP) — clock skew is the most common cause","Re-run the job or step: GitHub normally issues tokens valid for well over a minute","Ensure requests to the OIDC endpoint are not cached by any intermediary","In tests, mint the mock JWT with exp = now + 3600 so it clears the 60-second leeway"],"exampleFix":"// before (test fixture token already expiring)\nlet token = test_jwt(unix_timestamp().unwrap()); // exp == now -> \"expires too soon\"\n\n// after\nlet token = test_jwt(unix_timestamp().unwrap() + 3600);","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let auth = match credential_call().await {\n    Ok(auth) => auth,\n    Err(report) if report.to_string().contains(\"expires too soon\") => {\n        // usually clock skew or a stale cached token: sync clock / retry the job once\n        tokio::time::sleep(std::time::Duration::from_secs(30)).await;\n        credential_call().await?\n    }\n    Err(report) => return Err(report),\n};","preventionTips":["Keep NTP enabled on self-hosted runners — clock skew is the usual cause","Do not cache responses from the GitHub OIDC endpoint in proxies","In tests, mint mock JWTs with exp = now + 3600"],"tags":["oidc","github-actions","jwt","token-expiry","auth"],"backgroundTag":"jwt-token-expired","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}