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:
parent
ad054d3c5e
commit
d54751a8ee
2 changed files with 24 additions and 3 deletions
22
auth/auth.go
22
auth/auth.go
|
|
@ -47,6 +47,21 @@ type RequireBearerTokenOptions struct {
|
||||||
ResourceMetadataURL string
|
ResourceMetadataURL string
|
||||||
// The required scopes.
|
// The required scopes.
|
||||||
Scopes []string
|
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{}
|
type tokenInfoKey struct{}
|
||||||
|
|
@ -131,9 +146,10 @@ func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenO
|
||||||
|
|
||||||
// Check expiration.
|
// Check expiration.
|
||||||
if tokenInfo.Expiration.IsZero() {
|
if tokenInfo.Expiration.IsZero() {
|
||||||
return nil, "token missing expiration", http.StatusUnauthorized
|
if opts == nil || !opts.AllowMissingExpiration {
|
||||||
}
|
return nil, "token missing expiration", http.StatusUnauthorized
|
||||||
if tokenInfo.Expiration.Before(time.Now()) {
|
}
|
||||||
|
} else if tokenInfo.Expiration.Before(time.Now()) {
|
||||||
return nil, "token expired", http.StatusUnauthorized
|
return nil, "token expired", http.StatusUnauthorized
|
||||||
}
|
}
|
||||||
return tokenInfo, "", 0
|
return tokenInfo, "", 0
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@ func TestVerify(t *testing.T) {
|
||||||
"no expiration", nil, "Bearer noexp",
|
"no expiration", nil, "Bearer noexp",
|
||||||
"token missing expiration", 401,
|
"token missing expiration", 401,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"no expiration with AllowMissingExpiration accepts",
|
||||||
|
&RequireBearerTokenOptions{AllowMissingExpiration: true}, "Bearer noexp",
|
||||||
|
"", 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"expired", nil, "Bearer expired",
|
"expired", nil, "Bearer expired",
|
||||||
"token expired", 401,
|
"token expired", 401,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue