fix(openid): require expires_in for token responses

While RFC 6749 specify this field as recommended:

> If omitted, the authorization server SHOULD provide the
> expiration time via other means or document the default value.

and equivalently the OIDC Core spec specifies the same field as optional,
we will explicitly enforce that these fields are returned from the AS.

This isn't a breaking change as the existing session refresh logic implicitly
depends on this field and its value.

While there are probably some providers that omit the `expires_in` field
or sets it to zero with the intent of returning access tokens that do not
expire, we assume these are relatively rare. We might revisit this
at some point in the future, should our assumptions be wrong.
This commit is contained in:
Trong Huu Nguyen
2025-06-11 13:07:18 +02:00
parent bf2f97f400
commit 052d310280
2 changed files with 12 additions and 1 deletions

View File

@@ -119,6 +119,9 @@ func (c *Client) RefreshGrant(ctx context.Context, refreshToken, previousIDToken
return nil, fmt.Errorf("unmarshalling token response: %w", err)
}
span.SetAttributes(attribute.Int64("oauth.token_expires_in_seconds", tokenResponse.ExpiresIn))
if tokenResponse.ExpiresIn <= 0 {
return nil, fmt.Errorf("invalid token response: expires_in must be greater than 0, got %d", tokenResponse.ExpiresIn)
}
// id_tokens may not always be returned from a refresh grant (OpenID Connect Core 12.1)
if tokenResponse.IDToken != "" {

View File

@@ -51,9 +51,17 @@ func NewTokens(src *oauth2.Token, jwks *jwk.Set, cfg openidconfig.Config, cookie
return nil, fmt.Errorf("validating id_token: %w", err)
}
expiry := src.Expiry
if expiry.IsZero() {
if src.ExpiresIn <= 0 {
return nil, fmt.Errorf("missing or zero value for expires_in in token response")
}
expiry = time.Now().Add(time.Duration(src.ExpiresIn) * time.Second)
}
return &Tokens{
AccessToken: src.AccessToken,
Expiry: src.Expiry,
Expiry: expiry,
IDToken: idToken,
RefreshToken: src.RefreshToken,
TokenType: src.TokenType,