jdx/mise · error · eyre::Report
GitHub Actions OIDC token expires too soon
Error message
GitHub Actions OIDC token expires too soon
What it means
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.
Source
Thrown at crates/mise-cache-core/src/lib.rs:749
.client
.get(self.request_url.clone())
.header(AUTHORIZATION, self.request_token.clone())
.send()
.await?
.error_for_status()?
.json()
.await?)
})
.await
.map_err(|err| {
eyre!(
"failed to acquire GitHub Actions OIDC token for audience {:?}: {err}",
self.audience
)
})?;
let expires_at = jwt_expiry(&response.value)?;
if expires_at <= now.saturating_add(REFRESH_LEEWAY_SECONDS) {
bail!("GitHub Actions OIDC token expires too soon");
}
let authorization = authorization_header(Some(&response.value))?
.ok_or_else(|| eyre!("GitHub Actions returned an empty OIDC token"))?;
*cached = Some(CachedOidcToken {
authorization: authorization.clone(),
expires_at,
});
Ok(authorization)
}
}
fn jwt_expiry(token: &str) -> Result<u64> {
let payload = token
.split('.')
.nth(1)
.ok_or_else(|| eyre!("GitHub Actions returned a malformed OIDC token"))?;
let payload = URL_SAFE_NO_PAD
.decode(payload)View on GitHub (pinned to 9dcfcaa0dc)
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
Example fix
// before (test fixture token already expiring) let token = test_jwt(unix_timestamp().unwrap()); // exp == now -> "expires too soon" // after let token = test_jwt(unix_timestamp().unwrap() + 3600);
Defensive patterns
Strategy: retry
Try / catch
let auth = match credential_call().await {
Ok(auth) => auth,
Err(report) if report.to_string().contains("expires too soon") => {
// usually clock skew or a stale cached token: sync clock / retry the job once
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
credential_call().await?
}
Err(report) => return Err(report),
}; Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- GitHub Actions OIDC request URL must use HTTPS
- `auth` for {key} is not `user:password`
- {bin} get failed for {server}: {}
- fetching {url} failed: {}{hint} {}
- starting blob upload failed: {} {}{}
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/48fc5d24d1e7914b.
Report an issue: GitHub.