Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ type RequireBearerTokenOptions struct {
ResourceMetadataURL string
// The required scopes.
Scopes []string
// 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{}
Expand Down Expand Up @@ -129,11 +142,15 @@ func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenO
}
}

// Check expiration.
// Check expiration with optional clock-skew tolerance.
if tokenInfo.Expiration.IsZero() {
return nil, "token missing expiration", http.StatusUnauthorized
}
if tokenInfo.Expiration.Before(time.Now()) {
skew := time.Duration(0)
if opts != nil {
skew = opts.ClockSkew
}
if tokenInfo.Expiration.Add(skew).Before(time.Now()) {
Comment on lines +149 to +153
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
skew := time.Duration(0)
if opts != nil {
skew = opts.ClockSkew
}
if tokenInfo.Expiration.Add(skew).Before(time.Now()) {
if opts == nil {
opts = &RequireBearerTokenOptions{}
}
if tokenInfo.Expiration.Add(opts.ClockSkew).Before(time.Now()) {

return nil, "token expired", http.StatusUnauthorized
}
return tokenInfo, "", 0
Expand Down
60 changes: 60 additions & 0 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,63 @@ func TestRequireBearerToken(t *testing.T) {
})
}
}

// TestRequireBearerToken_ClockSkew verifies that the ClockSkew option
// extends the expiration check tolerance: a token whose Expiration is in the
// recent past is accepted iff the elapsed interval is within ClockSkew.
func TestRequireBearerToken_ClockSkew(t *testing.T) {
tests := []struct {
name string
clockSkew time.Duration
expiredAgo time.Duration
wantStatus int
}{
{
name: "no skew, fresh token accepted",
clockSkew: 0,
expiredAgo: -time.Minute, // expires in 1 minute
wantStatus: http.StatusOK,
},
{
name: "no skew, expired token rejected",
clockSkew: 0,
expiredAgo: 5 * time.Second, // expired 5s ago
wantStatus: http.StatusUnauthorized,
},
{
name: "with skew, recently-expired token accepted",
clockSkew: 30 * time.Second,
expiredAgo: 5 * time.Second,
wantStatus: http.StatusOK,
},
{
name: "with skew, token expired beyond tolerance rejected",
clockSkew: 10 * time.Second,
expiredAgo: 30 * time.Second,
wantStatus: http.StatusUnauthorized,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
verifier := func(_ context.Context, _ string, _ *http.Request) (*TokenInfo, error) {
return &TokenInfo{Expiration: time.Now().Add(-tt.expiredAgo)}, nil
}
handler := RequireBearerToken(verifier, &RequireBearerTokenOptions{
ClockSkew: tt.clockSkew,
})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))

req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer anything")
rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)

if rec.Code != tt.wantStatus {
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
}
})
}
}