From 22cab54360403e8a810c0cf8e65b349236611769 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 7 Aug 2024 10:47:02 -0500 Subject: [PATCH] make token expiry delta configurable (#73) This removes reuse token logic from within token sources and instead uses the oauth2.ReuseTokenSourceWithExpiry which supports configuring an early expiration and refresh of a token. Signed-off-by: Mike Mason --- chart/iam-runtime-infratographer/README.md | 4 +- chart/iam-runtime-infratographer/values.yaml | 5 +- internal/accesstoken/config.go | 5 ++ internal/accesstoken/tokenexchange.go | 49 +++----------------- internal/accesstoken/tokensource.go | 2 + internal/filetokensource/config.go | 14 +----- internal/filetokensource/token.go | 21 ++------- internal/filetokensource/token_test.go | 30 ------------ 8 files changed, 23 insertions(+), 107 deletions(-) diff --git a/chart/iam-runtime-infratographer/README.md b/chart/iam-runtime-infratographer/README.md index e87126ed..8f93174a 100644 --- a/chart/iam-runtime-infratographer/README.md +++ b/chart/iam-runtime-infratographer/README.md @@ -48,7 +48,7 @@ iam-runtime-infratographer: | Repository | Name | Version | |------------|------|---------| -| https://charts.bitnami.com/bitnami | common | 2.19.2 | +| https://charts.bitnami.com/bitnami | common | 2.20.5 | ## Values @@ -58,10 +58,10 @@ iam-runtime-infratographer: | config.accessToken.exchange.grantType | string | urn:ietf:params:oauth:grant-type:token-exchange | grantType configures the grant type | | config.accessToken.exchange.issuer | string | `""` | issuer specifies the URL for the issuer for the exchanged token. The Issuer must support OpenID discovery to discover the token endpoint. | | config.accessToken.exchange.tokenType | string | urn:ietf:params:oauth:token-type:jwt | tokenType configures the token type | +| config.accessToken.expiryDelta | duration | 10s | expiryDelta sets early expiry validation for the token. | | config.accessToken.source.clientCredentials.clientID | string | `""` | clientID is the client credentials id which is used to retrieve a token from the issuer. This attribute also supports a file path by prefixing the value with `file://`. example: `file:///var/secrets/client-id` | | config.accessToken.source.clientCredentials.clientSecret | string | `""` | clientSecret is the client credentials secret which is used to retrieve a token from the issuer. This attribute also supports a file path by prefixing the value with `file://`. example: `file:///var/secrets/client-secret` | | config.accessToken.source.clientCredentials.issuer | string | `""` | issuer specifies the URL for the issuer for the token request. The Issuer must support OpenID discovery to discover the token endpoint. | -| config.accessToken.source.fileToken.noReuseToken | bool | `false` | noReuseToken if enabled disables reuse of tokens while they're still valid. | | config.accessToken.source.fileToken.tokenPath | string | `""` | tokenPath is the path to the source jwt token. | | config.events.enabled | bool | `false` | enabled enables NATS event-based functions. | | config.events.nats.credsFile | string | `""` | credsFile path to NATS credentials file | diff --git a/chart/iam-runtime-infratographer/values.yaml b/chart/iam-runtime-infratographer/values.yaml index 7c1c9d4a..e91b43b1 100644 --- a/chart/iam-runtime-infratographer/values.yaml +++ b/chart/iam-runtime-infratographer/values.yaml @@ -40,12 +40,13 @@ config: accessToken: # -- enabled configures the access token source for GetAccessToken requests. enabled: false + # -- (duration) expiryDelta sets early expiry validation for the token. + # @default -- 10s + expiryDelta: 0 source: fileToken: # -- tokenPath is the path to the source jwt token. tokenPath: "" - # -- noReuseToken if enabled disables reuse of tokens while they're still valid. - noReuseToken: false clientCredentials: # -- issuer specifies the URL for the issuer for the token request. # The Issuer must support OpenID discovery to discover the token endpoint. diff --git a/internal/accesstoken/config.go b/internal/accesstoken/config.go index c0318fd0..86fe08b8 100644 --- a/internal/accesstoken/config.go +++ b/internal/accesstoken/config.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/url" + "time" "go.infratographer.com/iam-runtime-infratographer/internal/filetokensource" "go.uber.org/multierr" @@ -42,6 +43,10 @@ type Config struct { // Exchange configures where tokens get exchanges at. // If Issuer is empty, token exchange is disabled. Exchange AccessTokenExchangeConfig + + // ExpiryDelta sets early expiry validation for the token. + // Default is 10 seconds. + ExpiryDelta time.Duration } // Validate ensures the config has been configured properly. diff --git a/internal/accesstoken/tokenexchange.go b/internal/accesstoken/tokenexchange.go index 8e13350d..bf49ef0c 100644 --- a/internal/accesstoken/tokenexchange.go +++ b/internal/accesstoken/tokenexchange.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "sync" "go.infratographer.com/iam-runtime-infratographer/internal/jwt" "golang.org/x/oauth2" @@ -33,67 +32,33 @@ const ( type exchangeTokenSource struct { cfg AccessTokenExchangeConfig ctx context.Context - mu sync.Mutex upstream oauth2.TokenSource - upstreamToken *oauth2.Token exchangeConfig oauth2.Config - token *oauth2.Token } // Token retrieves an OAuth 2.0 access token from the configured issuer using token exchange. -// Tokens are reused as long as they are valid. -// Upstream tokens used as the source for the exchange are reused as long as they are valid. func (s *exchangeTokenSource) Token() (*oauth2.Token, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if s.token != nil && s.token.Valid() { - return s.token, nil - } - - if err := s.refreshUpstream(); err != nil { - return s.token, err - } - - if err := s.exchange(); err != nil { - return s.token, err - } - - return s.token, nil -} - -func (s *exchangeTokenSource) refreshUpstream() error { - if s.upstreamToken == nil || !s.upstreamToken.Valid() { - token, err := s.upstream.Token() - if err != nil { - return fmt.Errorf("%w: %w", ErrUpstreamTokenRequestFailed, err) - } - - s.upstreamToken = token + upstreamToken, err := s.upstream.Token() + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrUpstreamTokenRequestFailed, err) } - return nil -} - -func (s *exchangeTokenSource) exchange() error { token, err := s.exchangeConfig.Exchange(s.ctx, "", oauth2.SetAuthURLParam("grant_type", s.cfg.GrantType), - oauth2.SetAuthURLParam("subject_token", s.upstreamToken.AccessToken), + oauth2.SetAuthURLParam("subject_token", upstreamToken.AccessToken), oauth2.SetAuthURLParam("subject_token_type", s.cfg.TokenType), ) if err != nil { if rErr, ok := err.(*oauth2.RetrieveError); ok { if rErr.Response.StatusCode == http.StatusBadRequest { - return fmt.Errorf("%w: %w", ErrInvalidTokenExchangeRequest, rErr) + return nil, fmt.Errorf("%w: %w", ErrInvalidTokenExchangeRequest, rErr) } } - return fmt.Errorf("%w: %w", ErrTokenExchangeRequestFailed, err) + return nil, fmt.Errorf("%w: %w", ErrTokenExchangeRequestFailed, err) } - s.token = token - - return nil + return token, nil } func newExchangeTokenSource(ctx context.Context, cfg AccessTokenExchangeConfig, upstream oauth2.TokenSource) (oauth2.TokenSource, error) { diff --git a/internal/accesstoken/tokensource.go b/internal/accesstoken/tokensource.go index c1d9d4d3..5d858429 100644 --- a/internal/accesstoken/tokensource.go +++ b/internal/accesstoken/tokensource.go @@ -26,6 +26,8 @@ func (c Config) toTokenSource(ctx context.Context) (oauth2.TokenSource, error) { } } + source = oauth2.ReuseTokenSourceWithExpiry(nil, source, c.ExpiryDelta) + return source, nil } diff --git a/internal/filetokensource/config.go b/internal/filetokensource/config.go index aba5dee8..4a9839a8 100644 --- a/internal/filetokensource/config.go +++ b/internal/filetokensource/config.go @@ -9,10 +9,6 @@ var ErrTokenPathRequired = errors.New("file token source: TokenPath required") type Config struct { // TokenPath is the path to the source jwt token. TokenPath string - - // NoReuseToken if enabled disables reusing of tokens while they're still valid. - // Each request to [TokenSource.Token] will result in the latest token being loaded. - NoReuseToken bool } // WithTokenPath returns a new Config with the provided token path defined. @@ -22,13 +18,6 @@ func (c Config) WithTokenPath(path string) Config { return c } -// ReuseToken returns a new Config with NoReuseToken defined. -func (c Config) ReuseToken(reuse bool) Config { - c.NoReuseToken = !reuse - - return c -} - // Configured returns true when TokenPath is defined. func (c Config) Configured() bool { return c.TokenPath != "" @@ -50,8 +39,7 @@ func (c Config) ToTokenSource() (*TokenSource, error) { } tokenSource := &TokenSource{ - path: c.TokenPath, - noReuseToken: c.NoReuseToken, + path: c.TokenPath, } if _, err := tokenSource.Token(); err != nil { diff --git a/internal/filetokensource/token.go b/internal/filetokensource/token.go index 333b3ddc..ecebd405 100644 --- a/internal/filetokensource/token.go +++ b/internal/filetokensource/token.go @@ -3,7 +3,6 @@ package filetokensource import ( "fmt" "os" - "sync" "time" "github.com/golang-jwt/jwt/v5" @@ -11,24 +10,12 @@ import ( ) // TokenSource implemenets oauth2.TokenSource returning the token from the provided path. -// Loaded tokens are reused type TokenSource struct { - mu sync.Mutex - path string - noReuseToken bool - token *oauth2.Token + path string } // Token returns the latest token from the configured path. -// Unless Config.NoReuseToken is true, tokens are reused while they're still valid. func (s *TokenSource) Token() (*oauth2.Token, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if !s.noReuseToken && s.token != nil && s.token.Valid() { - return s.token, nil - } - tokenb, err := os.ReadFile(s.path) if err != nil { return nil, fmt.Errorf("error reading token file: %w", err) @@ -53,11 +40,9 @@ func (s *TokenSource) Token() (*oauth2.Token, error) { expiryTime = expiry.Time } - s.token = &oauth2.Token{ + return &oauth2.Token{ AccessToken: newToken, TokenType: "Bearer", Expiry: expiryTime, - } - - return s.token, nil + }, nil } diff --git a/internal/filetokensource/token_test.go b/internal/filetokensource/token_test.go index 36ac65ab..ffdaecae 100644 --- a/internal/filetokensource/token_test.go +++ b/internal/filetokensource/token_test.go @@ -46,41 +46,11 @@ func TestToken(t *testing.T) { expectSubject: "token1", }}, }, - { - "reuse token", - &TokenSource{}, - []fileToken{ - {subject: "token1", expectSubject: "token1"}, - {subject: "token2", expectSubject: "token1"}, - }, - }, { "no reuse token", - &TokenSource{ - noReuseToken: true, - }, - []fileToken{ - {subject: "token1", expectSubject: "token1"}, - {subject: "token2", expectSubject: "token2"}, - }, - }, - { - "reuse: no error returned", &TokenSource{}, []fileToken{ {subject: "token1", expectSubject: "token1"}, - {subject: "", expectSubject: "token1"}, - {subject: "token2", expectSubject: "token1"}, - }, - }, - { - "no reuse: error returned", - &TokenSource{ - noReuseToken: true, - }, - []fileToken{ - {subject: "token1", expectSubject: "token1"}, - {subject: "", expectError: jwt.ErrTokenMalformed}, {subject: "token2", expectSubject: "token2"}, }, },