auth: add AllowMissingExpiration option to RequireBearerTokenOptions (#971)

## 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) <noreply@anthropic.com>
Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
This commit is contained in:
BorisTyshkevich 2026-06-02 10:12:44 +02:00 • committed by GitHub
commit d54751a8ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 24 additions and 3 deletions

View file

@ -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

View file

@ -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,