Debug OAuth Token Clock Skew
Compare token issued-at, not-before, and expiry against server clocks to prove intermittent 401s come from time drift, not bad tokens.
Task
Measure clock differences between the issuer and your hosts and correct for skew when validating OAuth tokens.
When to use this
A cluster intermittently rejects freshly issued tokens with 401, while other clusters accept them, hinting at unsynchronised clocks.
Prerequisites
- Access to decode JWT claims
- NTP status on the affected hosts
- A staging issuer that can mint tokens on demand
Steps
1. Compare each host clock against the issuer clock.
date -u +%s
# issuer reported time
curl -s https://issuer.example.com/.well-known/openid-configuration >/dev/null
echo "compare with NTP: $(timedatectl show -p NTPSynchronized --value)"Expected output: Hosts whose time drifts beyond a few seconds flag NTPSynchronized=false or a visible offset.
2. Inspect nbf and exp claims around the failure window.
const { nbf, exp, iat } = JSON.parse(decodeJwtPayload(token));
const now = Math.floor(Date.now() / 1000);
console.log({ skewNbf: now - nbf, lifetime: exp - iat });Expected output: A negative or tiny skewNbf confirms the host clock is ahead, causing premature not-before rejection.
Troubleshooting
Problem
Only one pod fails
Solution
Check that pod host clock and NTP daemon; drift is per-host, not per-service.
Problem
Skew within tolerance but still rejected
Solution
Also verify the issuer and validator agree on the same time unit and that leeway is configured.
Limitations
- Adding validation leeway masks but does not fix drift.
- Does not address token replay or signature issues.
- Requires the ability to inspect claims without exposing secrets.
What this recipe does
Measure clock differences between the issuer and your hosts and correct for skew when validating OAuth tokens.
A cluster intermittently rejects freshly issued tokens with 401, while other clusters accept them, hinting at unsynchronised clocks.
Prerequisites
- Access to decode JWT claims
- NTP status on the affected hosts
- A staging issuer that can mint tokens on demand
Procedure
- Compare each host clock against the issuer clock.
date -u +%s
# issuer reported time
curl -s https://issuer.example.com/.well-known/openid-configuration >/dev/null
echo "compare with NTP: $(timedatectl show -p NTPSynchronized --value)"
Expected result: Hosts whose time drifts beyond a few seconds flag NTPSynchronized=false or a visible offset.
- Inspect nbf and exp claims around the failure window.
const { nbf, exp, iat } = JSON.parse(decodeJwtPayload(token));
const now = Math.floor(Date.now() / 1000);
console.log({ skewNbf: now - nbf, lifetime: exp - iat });
Expected result: A negative or tiny skewNbf confirms the host clock is ahead, causing premature not-before rejection.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows affected hosts synchronise via NTP and the 401 rate for fresh tokens returns to zero.
Troubleshooting
Only one pod fails — Check that pod host clock and NTP daemon; drift is per-host, not per-service.
Skew within tolerance but still rejected — Also verify the issuer and validator agree on the same time unit and that leeway is configured.
Limitations
- Adding validation leeway masks but does not fix drift.
- Does not address token replay or signature issues.
- Requires the ability to inspect claims without exposing secrets.