Skip to content

Commit

Permalink
make token expiry delta configurable (#73)
Browse files Browse the repository at this point in the history
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 <mimason@equinix.com>
  • Loading branch information
mikemrm authored Aug 7, 2024
1 parent 1a23239 commit 22cab54
Show file tree
Hide file tree
Showing 8 changed files with 23 additions and 107 deletions.
4 changes: 2 additions & 2 deletions chart/iam-runtime-infratographer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 |
Expand Down
5 changes: 3 additions & 2 deletions chart/iam-runtime-infratographer/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions internal/accesstoken/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/url"
"time"

"go.infratographer.com/iam-runtime-infratographer/internal/filetokensource"
"go.uber.org/multierr"
Expand Down Expand Up @@ -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.
Expand Down
49 changes: 7 additions & 42 deletions internal/accesstoken/tokenexchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/http"
"sync"

"go.infratographer.com/iam-runtime-infratographer/internal/jwt"
"golang.org/x/oauth2"
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions internal/accesstoken/tokensource.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func (c Config) toTokenSource(ctx context.Context) (oauth2.TokenSource, error) {
}
}

source = oauth2.ReuseTokenSourceWithExpiry(nil, source, c.ExpiryDelta)

return source, nil
}

Expand Down
14 changes: 1 addition & 13 deletions internal/filetokensource/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 != ""
Expand All @@ -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 {
Expand Down
21 changes: 3 additions & 18 deletions internal/filetokensource/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,19 @@ package filetokensource
import (
"fmt"
"os"
"sync"
"time"

"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
)

// 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)
Expand All @@ -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
}
30 changes: 0 additions & 30 deletions internal/filetokensource/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
},
},
Expand Down

0 comments on commit 22cab54

Please sign in to comment.