auth: add ClockSkew option to RequireBearerTokenOptions (#969)

## Summary

- Add a \`ClockSkew time.Duration\` field to
\`RequireBearerTokenOptions\` that bounds the tolerance applied to a
token's \`Expiration\` when checking elapsed validity.
- Default zero preserves the existing strict comparison; positive values
let resource servers tolerate the drift typical of distributed
deployments and cloud-managed IdPs.

## Motivation

Real-world resource servers running behind a CDN, in distributed
deployments, or talking to an authorization server whose clock drifts a
few seconds (common with cloud-managed IdPs) need a small positive
tolerance when checking token expiration. Strict-equality comparison
rejects tokens that are valid by the issuer's clock but momentarily
appear expired by the verifier's. The reverse case — an issuer's clock
running slightly fast at /token issuance — also reaches the verifier as
a token whose \`exp\` lies microseconds in the past.

## Test plan

- [x] \`go test ./auth/...\` passes.
- [x] New \`TestRequireBearerToken_ClockSkew\` exercises four cases:
fresh accept, strict-expired reject, within-skew accept, beyond-skew
reject.
- [x] Existing \`TestRequireBearerToken\` continues to pass with
\`ClockSkew\` defaulting to zero.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
This commit is contained in:
BorisTyshkevich 2026-06-05 12:16:43 +02:00 • committed by GitHub
commit 5045d86fb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -62,6 +62,19 @@ type RequireBearerTokenOptions struct {
// validity check it can perform; this option only relaxes the middleware's
// own expiration enforcement.
AllowMissingExpiration bool
// ClockSkew bounds the tolerance applied to a token's Expiration when
// deciding whether it has elapsed. A token is rejected only if
// Expiration + ClockSkew is before the current time. Zero (the default)
// preserves strict comparison: any expired token is rejected immediately.
//
// Resource servers running behind a CDN, in distributed deployments, or
// communicating with an authorization server whose clock drifts a few
// seconds (common with cloud-managed IdPs) need a small positive value
// here to avoid rejecting tokens that are valid by the issuer's clock
// but momentarily appear expired by the verifier's. The same tolerance
// guards against an issuer's clock running slightly fast at /token
// issuance time.
ClockSkew time.Duration
}
type tokenInfoKey struct{}
@ -144,12 +157,17 @@ func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenO
}
}
// Check expiration.
if opts == nil {
opts = &RequireBearerTokenOptions{}
}
// Check expiration, with optional clock-skew tolerance. Skew only applies
// when an expiration is present; a missing expiration is governed solely by
// AllowMissingExpiration.
if tokenInfo.Expiration.IsZero() {
if opts == nil || !opts.AllowMissingExpiration {
if !opts.AllowMissingExpiration {
return nil, "token missing expiration", http.StatusUnauthorized
}
} else if tokenInfo.Expiration.Before(time.Now()) {
} else if tokenInfo.Expiration.Add(opts.ClockSkew).Before(time.Now()) {
return nil, "token expired", http.StatusUnauthorized
}
return tokenInfo, "", 0