From d54751a8eec734558a36560faa1e19e9c966e5ee Mon Sep 17 00:00:00 2001 From: BorisTyshkevich <125603338+BorisTyshkevich@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:12:44 +0200 Subject: [PATCH] auth: add AllowMissingExpiration option to RequireBearerTokenOptions (#971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add an `AllowMissingExpiration bool` field to `RequireBearerTokenOptions` that opts the middleware out of the existing `tokenInfo.Expiration.IsZero()` reject. - Default false preserves the existing strict behaviour: every TokenInfo must carry an Expiration. ## Motivation Some IdPs emit session-bound bearer tokens that do not carry a standalone `exp` claim — the token's lifetime is bounded by an external session and is not advertised in-band. Resource servers integrating with such IdPs today have to synthesise a fake Expiration from their TokenVerifier callback (dishonest data through the auth layer) or fork `RequireBearerToken` to bypass the check. This option lets the verifier honestly report "no expiration in the token" via a zero `Expiration` and have the middleware accept it — while still applying the elapsed-validity check for tokens that DO carry an Expiration. Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Guglielmo Colombo --- auth/auth.go | 22 +++++++++++++++++++--- auth/auth_test.go | 5 +++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 40fa259..f205870 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -47,6 +47,21 @@ type RequireBearerTokenOptions struct { ResourceMetadataURL string // The required scopes. Scopes []string + // AllowMissingExpiration opts the middleware out of the + // `tokenInfo.Expiration.IsZero()` reject. Default false preserves the + // existing strict behaviour (every TokenInfo must carry an Expiration). + // + // Some IdPs emit session-bound bearer tokens that do not carry a standalone + // `exp` claim — the token's lifetime is bounded by an external session and + // is not advertised in-band. Resource servers integrating with such IdPs + // need to opt in to validating the rest of the token (scopes, signature + // via the verifier callback, etc.) without requiring the expiration field + // to be present. + // + // When enabled, the verifier is still responsible for any session-level + // validity check it can perform; this option only relaxes the middleware's + // own expiration enforcement. + AllowMissingExpiration bool } type tokenInfoKey struct{} @@ -131,9 +146,10 @@ func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenO // Check expiration. if tokenInfo.Expiration.IsZero() { - return nil, "token missing expiration", http.StatusUnauthorized - } - if tokenInfo.Expiration.Before(time.Now()) { + if opts == nil || !opts.AllowMissingExpiration { + return nil, "token missing expiration", http.StatusUnauthorized + } + } else if tokenInfo.Expiration.Before(time.Now()) { return nil, "token expired", http.StatusUnauthorized } return tokenInfo, "", 0 diff --git a/auth/auth_test.go b/auth/auth_test.go index 4028c90..fe523a1 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -62,6 +62,11 @@ func TestVerify(t *testing.T) { "no expiration", nil, "Bearer noexp", "token missing expiration", 401, }, + { + "no expiration with AllowMissingExpiration accepts", + &RequireBearerTokenOptions{AllowMissingExpiration: true}, "Bearer noexp", + "", 0, + }, { "expired", nil, "Bearer expired", "token expired", 401,