Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Token lifecycle updates#50

Merged
tylercreller merged 1 commit intoproject-kessel:mainfrom
tylercreller:token-optimizations
Mar 6, 2026
Merged

Token lifecycle updates#50
tylercreller merged 1 commit intoproject-kessel:mainfrom
tylercreller:token-optimizations

Conversation

@tylercreller
Copy link
Copy Markdown
Member

@tylercreller tylercreller commented Mar 5, 2026

  • Reuse one http.Client per TokenClient for TLS connection pooling
  • Add GetTokenWithContext() for caller controlled cancellation and timeouts
  • Cache tokens based on actual expires_in from SSO instead of hardcoded 5 minutes
    • Tokens with ExpiresIn <= 30s are not cached
  • Add configurable TokenHTTPTimeout (default 10s)
  • GetToken() still backwards compatible, wraps GetTokenWithContext()
  • Add unit tests

CI is not setup in this repo yet, pasting test runs

❯ go test ./common/ -v -count=1
=== RUN   TestGetTokenWithContext_FetchesFromServer
--- PASS: TestGetTokenWithContext_FetchesFromServer (0.00s)
=== RUN   TestGetTokenWithContext_ReturnsCachedToken
--- PASS: TestGetTokenWithContext_ReturnsCachedToken (0.00s)
=== RUN   TestGetTokenWithContext_RefetchesExpiredToken
--- PASS: TestGetTokenWithContext_RefetchesExpiredToken (0.00s)
=== RUN   TestGetTokenWithContext_ContextCancellation
--- PASS: TestGetTokenWithContext_ContextCancellation (1.00s)
=== RUN   TestGetTokenWithContext_ServerError
--- PASS: TestGetTokenWithContext_ServerError (0.00s)
=== RUN   TestGetTokenWithContext_CacheDurationRespectsExpiresIn
--- PASS: TestGetTokenWithContext_CacheDurationRespectsExpiresIn (0.00s)
=== RUN   TestGetTokenWithContext_DoesNotCacheShortLivedTokens
--- PASS: TestGetTokenWithContext_DoesNotCacheShortLivedTokens (0.00s)
=== RUN   TestGetToken_BackwardCompatible
--- PASS: TestGetToken_BackwardCompatible (0.00s)
=== RUN   TestNewTokenClient_DefaultTimeout
--- PASS: TestNewTokenClient_DefaultTimeout (0.00s)
=== RUN   TestNewTokenClient_CustomTimeout
--- PASS: TestNewTokenClient_CustomTimeout (0.00s)
=== RUN   TestGetTokenWithContext_ReusesHTTPClient
--- PASS: TestGetTokenWithContext_ReusesHTTPClient (0.00s)
=== RUN   TestIsJWTTokenExpired_ExpiredToken
--- PASS: TestIsJWTTokenExpired_ExpiredToken (0.00s)
=== RUN   TestIsJWTTokenExpired_ValidToken
--- PASS: TestIsJWTTokenExpired_ValidToken (0.00s)
=== RUN   TestIsJWTTokenExpired_EmptyString
--- PASS: TestIsJWTTokenExpired_EmptyString (0.00s)
=== RUN   TestGetTokenWithContext_SendsCorrectFormData
--- PASS: TestGetTokenWithContext_SendsCorrectFormData (0.01s)
PASS
ok      github.com/project-kessel/inventory-client-go/common    1.338s

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 5, 2026

Reviewer's Guide

Refactors the token client to use a reusable HTTP client with configurable timeout, introduces context-aware token retrieval with proper cancellation, and updates token caching to honor SSO-provided expiry times while adding comprehensive unit tests around token lifecycle behavior.

Sequence diagram for GetTokenWithContext token retrieval and caching

sequenceDiagram
    actor Caller
    participant TokenClient
    participant Cache as cache.Cache
    participant HTTPClient as http.Client
    participant SSO as AuthServer

    Caller->>TokenClient: GetTokenWithContext(ctx)
    TokenClient->>TokenClient: build cachedTokenKey
    TokenClient->>Cache: Get(cachedTokenKey)
    Cache-->>TokenClient: token, found?
    alt token present and not expired
        TokenClient-->>Caller: TokenResponse(AccessToken from cache)
    else cache miss or expired
        TokenClient->>TokenClient: build form data
        TokenClient->>HTTPClient: Do(POST url, ctx)
        HTTPClient->>SSO: POST /token
        SSO-->>HTTPClient: 200 OK, JSON token
        HTTPClient-->>TokenClient: http.Response
        TokenClient->>TokenClient: json.Unmarshal(body, TokenResponse)
        TokenClient->>TokenClient: cacheDuration = ExpiresIn - tokenExpiryMargin
        TokenClient->>Cache: Set(cachedTokenKey, AccessToken, cacheDuration)
        TokenClient-->>Caller: TokenResponse
    end
Loading

Class diagram for updated token client and config

classDiagram
    class Config {
        +string clientId
        +string clientSecret
        +string authServerTokenUrl
        +*tls.Config TlsConfig
        +time.Duration Timeout
        +time.Duration TokenHTTPTimeout
    }

    class TokenClient {
        -string clientId
        -string clientSecret
        -string url
        -bool EnableOIDCAuth
        -bool Insecure
        -cache.Cache cache
        -http.Client httpClient
        +TokenClient(config *Config)
        +GetCachedToken(tokenKey string) (string, error)
        +GetToken() (*TokenResponse, error)
        +GetTokenWithContext(ctx context.Context) (*TokenResponse, error)
    }

    class TokenResponse {
        +string AccessToken
        +int ExpiresIn
    }

    class cache.Cache {
        +Set(key string, value interface, duration time.Duration)
        +Get(key string) (interface, bool)
    }

    class http.Client {
        +time.Duration Timeout
        +Do(req *http.Request) (*http.Response, error)
    }

    Config --> TokenClient : used to construct
    TokenClient --> cache.Cache : uses
    TokenClient --> http.Client : holds reusable
    TokenClient --> TokenResponse : returns
Loading

File-Level Changes

Change Details Files
Introduce reusable HTTP client with configurable timeout for TokenClient and wire through configuration.
  • Add defaultTokenHTTPTimeout constant and TokenHTTPTimeout field to Config
  • Extend NewTokenClient to read TokenHTTPTimeout from Config, falling back to default, and construct an http.Client with that timeout
  • Store the constructed http.Client on TokenClient and reuse it for token requests
  • Rename cacheCleanupInterval constant to defaultCacheCleanupInterval to reflect its purpose
common/token.go
common/config.go
common/token_test.go
Add context-aware token retrieval while keeping GetToken backwards compatible.
  • Change GetToken to delegate to new GetTokenWithContext using context.Background()
  • Implement GetTokenWithContext to accept a context and use http.NewRequestWithContext
  • Ensure the TokenClient HTTP call uses the shared httpClient instead of creating a new client per call
common/token.go
common/token_test.go
Update token caching behavior to respect SSO-provided expiry with a safety margin.
  • Initialize token cache with NoExpiration and a default cleanup interval
  • Compute cache duration from TokenResponse.ExpiresIn in seconds and subtract a fixed safety margin when greater than the margin
  • Use the computed cacheDuration when storing tokens in the cache
  • Simplify token cache key construction and variable naming (cachedTokenKey, isExpired)
common/token.go
common/token_test.go
Add unit tests covering token lifecycle, HTTP client reuse, context behavior, and configuration.
  • Add tests for fetching tokens from server, using cached tokens, and refetching when expired
  • Add tests for context cancellation, server error handling, and cache expiration based on expires_in minus margin
  • Add tests verifying GetToken remains backward compatible and that NewTokenClient honors default and custom HTTP timeouts
  • Add tests for HTTP client reuse, JWT expiry helper behavior, and correctness of form data sent in token requests
common/token_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • When tokenResponse.ExpiresIn is zero or missing, cacheDuration becomes 0 and go-cache will treat this as NoExpiration, meaning the token may be cached indefinitely even if the JWT has an expiry; consider explicitly handling ExpiresIn <= 0 (e.g. by not caching, or falling back to a sane default duration).
  • The cache is now created with NoExpiration as the default and per-item TTL derived from expires_in; it may be worth guarding against very large expires_in values (e.g. hours/days) to avoid holding stale tokens too long if the server misconfigures or changes expiry semantics.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When `tokenResponse.ExpiresIn` is zero or missing, `cacheDuration` becomes 0 and `go-cache` will treat this as `NoExpiration`, meaning the token may be cached indefinitely even if the JWT has an expiry; consider explicitly handling `ExpiresIn <= 0` (e.g. by not caching, or falling back to a sane default duration).
- The cache is now created with `NoExpiration` as the default and per-item TTL derived from `expires_in`; it may be worth guarding against very large `expires_in` values (e.g. hours/days) to avoid holding stale tokens too long if the server misconfigures or changes expiry semantics.

## Individual Comments

### Comment 1
<location path="common/token.go" line_range="155-158" />
<code_context>
 	}
-	a.cache.Set(cachedTokenKey, tokenResponse.AccessToken, cacheCleanupInterval)
+
+	// Cache based on actual token lifetime from SSO, with a safety margin
+	// to ensure we refresh before expiry.
+	cacheDuration := time.Duration(tokenResponse.ExpiresIn) * time.Second
+	if cacheDuration > tokenExpiryMargin {
+		cacheDuration -= tokenExpiryMargin
+	}
</code_context>
<issue_to_address>
**issue (bug_risk):** Handling of short-lived tokens can lead to them being cached indefinitely

When `ExpiresIn <= tokenExpiryMargin`, `cacheDuration` becomes zero or negative. go-cache treats any negative (non-zero) duration as "no expiration", so very short-lived tokens would be cached indefinitely, which is a behavior change from the previous fixed TTL and likely unintended.

Consider either:
- not caching when `ExpiresIn <= tokenExpiryMargin`, or
- clamping to a minimal positive TTL when `cacheDuration <= 0`, or
- using `cache.DefaultExpiration` in that case and configuring a sane default TTL instead of `NoExpiration`.
</issue_to_address>

### Comment 2
<location path="common/token_test.go" line_range="14-18" />
<code_context>
+	"github.com/golang-jwt/jwt/v5"
+)
+
+func createTestJWT(exp time.Time) string {
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+		"exp": exp.Unix(),
+	})
+	signed, _ := token.SignedString([]byte("test-secret"))
+	return signed
+}
</code_context>
<issue_to_address>
**suggestion (testing):** Handle potential error from JWT signing in tests instead of ignoring it

`createTestJWT` currently discards the error from `SignedString`, which could mask test setup failures if the signing method or key changes. Update this helper to either return `(string, error)` or accept a `*testing.T` and call `t.Fatalf` (marking it as `t.Helper()`), so any signing error fails the test clearly.

Suggested implementation:

```golang
func createTestJWT(t *testing.T, exp time.Time) string {
	t.Helper()

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"exp": exp.Unix(),
	})

	signed, err := token.SignedString([]byte("test-secret"))
	if err != nil {
		t.Fatalf("failed to sign test JWT: %v", err)
	}

	return signed
}

```

Update all call sites of `createTestJWT` in `common/token_test.go` (and any other test files if present) to pass a `*testing.T`:

- Change calls like `createTestJWT(exp)` to `createTestJWT(t, exp)`, where `t` is the test's `*testing.T`.
- If `createTestJWT` is used inside helper functions that don't currently receive `*testing.T`, update those helpers to accept `*testing.T` and thread it through to `createTestJWT`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread common/token.go
Comment thread common/token_test.go Outdated
@tylercreller tylercreller force-pushed the token-optimizations branch 2 times, most recently from c93692b to 441a90f Compare March 5, 2026 21:57
@tylercreller tylercreller force-pushed the token-optimizations branch from 441a90f to 7be2021 Compare March 5, 2026 21:59
@tylercreller tylercreller merged commit 8afa3f9 into project-kessel:main Mar 6, 2026
2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants