Skip to content

Commit 5045d86

Browse files
BorisTyshkevichclaudeguglielmo-san
authored
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>
1 parent c60a318 commit 5045d86

2 files changed

Lines changed: 81 additions & 3 deletions

File tree

auth/auth.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ type RequireBearerTokenOptions struct {
6262
// validity check it can perform; this option only relaxes the middleware's
6363
// own expiration enforcement.
6464
AllowMissingExpiration bool
65+
// ClockSkew bounds the tolerance applied to a token's Expiration when
66+
// deciding whether it has elapsed. A token is rejected only if
67+
// Expiration + ClockSkew is before the current time. Zero (the default)
68+
// preserves strict comparison: any expired token is rejected immediately.
69+
//
70+
// Resource servers running behind a CDN, in distributed deployments, or
71+
// communicating with an authorization server whose clock drifts a few
72+
// seconds (common with cloud-managed IdPs) need a small positive value
73+
// here to avoid rejecting tokens that are valid by the issuer's clock
74+
// but momentarily appear expired by the verifier's. The same tolerance
75+
// guards against an issuer's clock running slightly fast at /token
76+
// issuance time.
77+
ClockSkew time.Duration
6578
}
6679

6780
type tokenInfoKey struct{}
@@ -144,12 +157,17 @@ func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenO
144157
}
145158
}
146159

147-
// Check expiration.
160+
if opts == nil {
161+
opts = &RequireBearerTokenOptions{}
162+
}
163+
// Check expiration, with optional clock-skew tolerance. Skew only applies
164+
// when an expiration is present; a missing expiration is governed solely by
165+
// AllowMissingExpiration.
148166
if tokenInfo.Expiration.IsZero() {
149-
if opts == nil || !opts.AllowMissingExpiration {
167+
if !opts.AllowMissingExpiration {
150168
return nil, "token missing expiration", http.StatusUnauthorized
151169
}
152-
} else if tokenInfo.Expiration.Before(time.Now()) {
170+
} else if tokenInfo.Expiration.Add(opts.ClockSkew).Before(time.Now()) {
153171
return nil, "token expired", http.StatusUnauthorized
154172
}
155173
return tokenInfo, "", 0

auth/auth_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,63 @@ func TestRequireBearerToken(t *testing.T) {
289289
})
290290
}
291291
}
292+
293+
// TestRequireBearerToken_ClockSkew verifies that the ClockSkew option
294+
// extends the expiration check tolerance: a token whose Expiration is in the
295+
// recent past is accepted iff the elapsed interval is within ClockSkew.
296+
func TestRequireBearerToken_ClockSkew(t *testing.T) {
297+
tests := []struct {
298+
name string
299+
clockSkew time.Duration
300+
expiredAgo time.Duration
301+
wantStatus int
302+
}{
303+
{
304+
name: "no skew, fresh token accepted",
305+
clockSkew: 0,
306+
expiredAgo: -time.Minute, // expires in 1 minute
307+
wantStatus: http.StatusOK,
308+
},
309+
{
310+
name: "no skew, expired token rejected",
311+
clockSkew: 0,
312+
expiredAgo: 5 * time.Second, // expired 5s ago
313+
wantStatus: http.StatusUnauthorized,
314+
},
315+
{
316+
name: "with skew, recently-expired token accepted",
317+
clockSkew: 30 * time.Second,
318+
expiredAgo: 5 * time.Second,
319+
wantStatus: http.StatusOK,
320+
},
321+
{
322+
name: "with skew, token expired beyond tolerance rejected",
323+
clockSkew: 10 * time.Second,
324+
expiredAgo: 30 * time.Second,
325+
wantStatus: http.StatusUnauthorized,
326+
},
327+
}
328+
329+
for _, tt := range tests {
330+
t.Run(tt.name, func(t *testing.T) {
331+
verifier := func(_ context.Context, _ string, _ *http.Request) (*TokenInfo, error) {
332+
return &TokenInfo{Expiration: time.Now().Add(-tt.expiredAgo)}, nil
333+
}
334+
handler := RequireBearerToken(verifier, &RequireBearerTokenOptions{
335+
ClockSkew: tt.clockSkew,
336+
})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
337+
w.WriteHeader(http.StatusOK)
338+
}))
339+
340+
req := httptest.NewRequest("GET", "/", nil)
341+
req.Header.Set("Authorization", "Bearer anything")
342+
rec := httptest.NewRecorder()
343+
344+
handler.ServeHTTP(rec, req)
345+
346+
if rec.Code != tt.wantStatus {
347+
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
348+
}
349+
})
350+
}
351+
}

0 commit comments

Comments
 (0)