← Recipes

阅读中文版本

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.

~30 minoauthsecuritydebugging

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

  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 result: Hosts whose time drifts beyond a few seconds flag NTPSynchronized=false or a visible offset.

  1. 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.