Skip to content

feat: derive token cache expiry from JWT claim#25

Open
drombosky wants to merge 1 commit into
tuannvm:mainfrom
drombosky:expire-jwts
Open

feat: derive token cache expiry from JWT claim#25
drombosky wants to merge 1 commit into
tuannvm:mainfrom
drombosky:expire-jwts

Conversation

@drombosky
Copy link
Copy Markdown

@drombosky drombosky commented Mar 20, 2026

Summary

Currently the code will cache a JWT for 5 minutes. This creates a timing bug and forces unnecessary caching.

Since tokens are cached for 5 minutes, it is possible that a token with 1 second of expiration left is cached. The cache code pulls the token out of the cache without any validation, the expired token will be used for the next 4 minutes and 59 seconds.

This work sets the cache expiration time based on the token expiration time. This results in each token being cached once and evicted from the cache at expiration.

In cases where the JWT needs to be valid beyond this check (e.g., for downstream requests), users can optionally choose to expire the cache before the JWT expiration time with TokenExpiryBuffer option. This option also ensures JWTs that are within the expiration window are treated as expired.

Changes

  • Adds a TokenExpiryBuffer time.Duration option to ensure tokens have a minimum lifetime after validation.
  • Updates ValidateToken in TokenValidator to return the expiry on the JWT (5 minutes if one is not present).
  • Updates server.Middleware() to use server.ValidateTokenCached to unify caching tokens.

Testing

  • Tests pass locally
  • New tests added (if applicable)

Summary by CodeRabbit

  • New Features

    • Added configurable token expiry buffer to control when tokens are treated as expired.
    • Cache lifetime now derives per-token from each JWT's expiry (adjusted by the buffer).
  • Behavior Changes

    • Token validation now yields token expiry, causing cache and middleware to honor per-token expiry and buffer; tokens expired or within the buffer are rejected.
    • Middleware simplified to delegate validation/caching and return validation errors directly.
  • Documentation

    • SECURITY guidance updated to document cache and buffer semantics.
  • Tests

    • Tests added/updated to cover caching, buffer behavior, expiry timing, and call-site signature changes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 441a1320-50ca-44ce-8fd8-c13d507ce712

📥 Commits

Reviewing files that changed from the base of the PR and between 2e12ae1 and dbb45ca.

📒 Files selected for processing (9)
  • api_test.go
  • config.go
  • context_propagation_test.go
  • docs/SECURITY.md
  • integration_test.go
  • middleware.go
  • oauth.go
  • provider/provider.go
  • provider/provider_test.go
✅ Files skipped from review due to trivial changes (1)
  • middleware.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • integration_test.go
  • docs/SECURITY.md
  • context_propagation_test.go
  • provider/provider_test.go
  • config.go
  • api_test.go
  • oauth.go
  • provider/provider.go

Walkthrough

Validators now return a token expiry; caching uses each token's JWT exp (optionally minus configurable TokenExpiryBuffer) to decide cache lifetime and reject near-expiry tokens. Middleware delegates to ValidateTokenCached. Tests and docs updated to exercise expiry and buffer behavior.

Changes

Cohort / File(s) Summary
Config & Builder
config.go
Added exported TokenExpiryBuffer time.Duration, validation to reject negative values, ConfigBuilder.WithTokenExpiryBuffer(d time.Duration), and env parsing for TOKEN_EXPIRY_BUFFER.
Provider interface & implementations
provider/provider.go, provider/...
Changed provider.TokenValidator.ValidateToken signature to (*User, time.Time, error). Updated HMACValidator and OIDCValidator to return an expiry (from exp or token metadata) and return time.Time{} on error paths.
Caching & OAuth logic
oauth.go
ValidateTokenCached now keys cache by token hash, consumes validator-provided expiry, subtracts optional TokenExpiryBuffer, rejects tokens with insufficient remaining lifetime, and sets cache entries to expire at the token-derived expiry. Logging and error handling adjusted.
Middleware
middleware.go
Removed inline token-hash and 5-minute TTL cache logic; middleware now calls s.ValidateTokenCached(ctx, tokenString) and returns validation errors directly while injecting user into context on success.
Tests — new and updated
api_test.go, context_propagation_test.go, integration_test.go, provider/provider_test.go
Added TestValidateTokenCached and TestValidateTokenCached_Expires in api_test.go. Updated many test call sites to handle the new (user, expiry, err) return by capturing the extra value (user, _, err). Adjusted assertions to validate expiry and buffer behavior.
Docs
docs/SECURITY.md
Replaced hardcoded 5-minute cache TTL wording with JWT-driven TTL (exp minus configurable buffer); documented fallback 5-minute cache for tokens missing exp and rejection of tokens within the buffer.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Middleware as Middleware
    participant ValidatorCache as ValidateTokenCached
    participant Validator as TokenValidator
    participant Cache as Cache

    Client->>Middleware: HTTP request with Bearer token
    Middleware->>ValidatorCache: ValidateTokenCached(ctx, token)

    alt Cache hit & not expired
        ValidatorCache->>Cache: lookup(token)
        Cache-->>ValidatorCache: cached user
        ValidatorCache-->>Middleware: return user
    else Cache miss or expired
        ValidatorCache->>Validator: ValidateToken(ctx, token)
        Validator-->>ValidatorCache: user, expiry, err
        alt validation succeeded
            ValidatorCache->>ValidatorCache: expiryAdj = expiry - TokenExpiryBuffer
            alt time.Now() < expiryAdj
                ValidatorCache->>Cache: set(token, user, ttl = expiry - now)
                ValidatorCache-->>Middleware: return user
            else token expired/expiring too soon
                ValidatorCache-->>Middleware: return auth error
            end
        else validation failed
            ValidatorCache-->>Middleware: return auth error
        end
    end

    Middleware-->>Client: respond with user in context or auth error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat/init #1: Introduced the ValidateToken signature change to include expiry and initial TokenExpiryBuffer/cache behavior that this PR extends and integrates with.

Poem

🐰 I hopped through claims and fields of time,
I nudged the expiry, set a buffered line,
No fixed five minutes — let JWTs decide,
Cache till the moment exp and buffer collide,
Hop, validate, and nibble a carrot wide. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: derive token cache expiry from JWT claim' directly and concisely summarizes the main change: replacing fixed 5-minute cache TTL with dynamic expiry derived from JWT exp claim.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@drombosky
Copy link
Copy Markdown
Author

This is my first time dealing with Cursor Code Review. It looks like there is a CI error. If there's something wrong with my PR, please feel free to point me in the right direction and I can fix it :)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
provider/provider_test.go (1)

26-53: Cover the no-exp fallback in this suite.

This only locks in the explicit-exp path. The interface change also relies on validators returning a non-zero fallback expiry when exp is absent, and ValidateTokenCached now depends on that contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@provider/provider_test.go` around lines 26 - 53, Add a subtest to cover the
no-`exp` fallback path: inside the existing t.Run("ValidAudience", ...) or as a
sibling test, create a JWT with the same valid audience and no "exp" claim and
call validator.ValidateToken (and optionally ValidateTokenCached) to assert it
returns a non-nil user and a non-zero expiration that equals the expected
fallback expiry used by the validator; reference validator.ValidateToken and
validator.ValidateTokenCached and assert expiration.Unix() (or
expiration.IsZero()) matches the fallback contract the validator implements.
provider/provider.go (1)

130-135: Prefer MapClaims.GetExpirationTime() over raw float64 cast.

This keeps cache-expiry extraction aligned with jwt/v5 claim parsing, includes proper type validation and error handling, and safely handles both float64 and json.Number values instead of silently falling back to the 5-minute default.

♻️ Proposed change
- expiry := time.Now().Add(5 * time.Minute)
- if expVal, ok := claims["exp"].(float64); ok {
- 	expiry = time.Unix(int64(expVal), 0)
- }
+ expiry := time.Now().Add(5 * time.Minute)
+ if expTime, err := claims.GetExpirationTime(); err == nil && expTime != nil {
+ 	expiry = expTime.Time
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@provider/provider.go` around lines 130 - 135, Replace the manual float64 cast
extraction of "exp" with jwt/v5's MapClaims.GetExpirationTime() API: assert or
convert your claims to jwt.MapClaims (the variable named claims) and call
claims.GetExpirationTime(); if it returns a valid time use that for expiry,
otherwise fall back to time.Now().Add(5*time.Minute) and log or handle the
parsing error as needed; update the expiry assignment logic where expiry is
currently set to ensure proper type validation and error handling instead of the
raw cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api_test.go`:
- Around line 713-783: The test never verifies the cached entry is stored with
the expected TTL/ExpiresAt because it only calls ValidateTokenCached once; after
filling the cache you must either call srv.cache.getCachedToken(tokenString) and
assert the cached item's ExpiresAt is near time.Now().Add(tt.expirationFromNow)
minus TokenExpiryBuffer (use Config.TokenExpiryBuffer) or invoke
ValidateTokenCached a second time after the original token's exp time window to
ensure the cache TTL prevented reuse (and assert error when within the buffer).
Update TestTokenExpiryBuffer to, for each success case, retrieve the cached
entry via srv.cache.getCachedToken or perform a second validation and assert the
stored ExpiresAt / resulting behavior matches the expected TokenExpiryBuffer
logic.

In `@config.go`:
- Around line 46-49: Reject negative TokenExpiryBuffer values by adding a
validation check where configuration is validated (e.g., the config validation
function that runs before using ValidateTokenCached). Specifically, ensure
TokenExpiryBuffer (the TokenExpiryBuffer field) must be >= 0 and return a clear
error if it's negative so ValidateTokenCached never receives a negative
duration; update any config parsing/initialization code to enforce this and
include the field name in the error message.

In `@docs/SECURITY.md`:
- Around line 203-204: Update the "Cache TTL" statement to explicitly document
two behaviors: when a JWT includes an exp claim use that expiry (capped by the
configurable expiry buffer), and when a JWT has no exp fall back to a 5-minute
cache TTL; also state that tokens whose expiry falls inside the configured
buffer are rejected (not cached). Reference the "Cache TTL" section and the JWT
`exp` claim wording and include mention of the configurable expiry buffer so
readers know where to change the buffer behavior.

In `@middleware.go`:
- Around line 39-42: The middleware is double-wrapping authentication errors
because ValidateTokenCached already returns errors prefixed with "authentication
failed"; update the handler around s.ValidateTokenCached(ctx, tokenString) to
log the error (using s.logger.Error with context req.Params.Name and err) but
return the original err instead of wrapping it (i.e., do not
fmt.Errorf("authentication failed: %w", err)); keep the existing logging call
and ensure the returned error is the one from ValidateTokenCached so callers
don't see duplicated prefixes.

In `@provider/provider.go`:
- Around line 85-86: The code uses strings.TrimPrefix(tokenString, "Bearer ")
which only matches canonical casing; update both places that call
strings.TrimPrefix(tokenString, "Bearer ") (the one in ValidateToken and the
other validator that also manipulates tokenString) to perform a case-insensitive
Bearer removal: detect the "bearer " prefix using a case-insensitive check
(e.g., strings.HasPrefix(strings.ToLower(tokenString), "bearer ")) and then
slice off the prefix so tokenString contains only the token; ensure you replace
both occurrences of strings.TrimPrefix(tokenString, "Bearer ") to keep behavior
consistent.

---

Nitpick comments:
In `@provider/provider_test.go`:
- Around line 26-53: Add a subtest to cover the no-`exp` fallback path: inside
the existing t.Run("ValidAudience", ...) or as a sibling test, create a JWT with
the same valid audience and no "exp" claim and call validator.ValidateToken (and
optionally ValidateTokenCached) to assert it returns a non-nil user and a
non-zero expiration that equals the expected fallback expiry used by the
validator; reference validator.ValidateToken and validator.ValidateTokenCached
and assert expiration.Unix() (or expiration.IsZero()) matches the fallback
contract the validator implements.

In `@provider/provider.go`:
- Around line 130-135: Replace the manual float64 cast extraction of "exp" with
jwt/v5's MapClaims.GetExpirationTime() API: assert or convert your claims to
jwt.MapClaims (the variable named claims) and call claims.GetExpirationTime();
if it returns a valid time use that for expiry, otherwise fall back to
time.Now().Add(5*time.Minute) and log or handle the parsing error as needed;
update the expiry assignment logic where expiry is currently set to ensure
proper type validation and error handling instead of the raw cast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00a0ca9e-8dcd-4b56-8e06-49658959cc56

📥 Commits

Reviewing files that changed from the base of the PR and between 0b83f5c and 2471b78.

📒 Files selected for processing (9)
  • api_test.go
  • config.go
  • context_propagation_test.go
  • docs/SECURITY.md
  • integration_test.go
  • middleware.go
  • oauth.go
  • provider/provider.go
  • provider/provider_test.go

Comment thread api_test.go Outdated
Comment thread config.go
Comment thread docs/SECURITY.md Outdated
Comment thread middleware.go Outdated
Comment thread provider/provider.go
Comment on lines 85 to 86
// Remove Bearer prefix if present
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 20, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle the Bearer prefix case-insensitively in both validators.

Authorization schemes are case-insensitive. strings.TrimPrefix(..., "Bearer ") only strips the canonical casing, so direct callers that pass bearer <token> into ValidateToken will fail verification even though the header is valid. Apply the same normalization in both implementations.

🩹 Suggested change
- tokenString = strings.TrimPrefix(tokenString, "Bearer ")
+ if len(tokenString) >= 7 && strings.EqualFold(tokenString[:7], "Bearer ") {
+ 	tokenString = tokenString[7:]
+ }
+ tokenString = strings.TrimSpace(tokenString)

Also applies to: 234-235

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@provider/provider.go` around lines 85 - 86, The code uses
strings.TrimPrefix(tokenString, "Bearer ") which only matches canonical casing;
update both places that call strings.TrimPrefix(tokenString, "Bearer ") (the one
in ValidateToken and the other validator that also manipulates tokenString) to
perform a case-insensitive Bearer removal: detect the "bearer " prefix using a
case-insensitive check (e.g., strings.HasPrefix(strings.ToLower(tokenString),
"bearer ")) and then slice off the prefix so tokenString contains only the
token; ensure you replace both occurrences of strings.TrimPrefix(tokenString,
"Bearer ") to keep behavior consistent.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not fixing since this is pre-existing code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@drombosky, understood — keeping the scope of this PR focused makes sense. Would you like me to open a follow-up GitHub issue to track the case-insensitive Bearer prefix handling so it isn't lost?


🧠 Learnings used
Learnt from: CR
Repo: tuannvm/oauth-mcp-proxy PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-13T17:03:53.298Z
Learning: Providers implement TokenValidator interface and all token validation happens in provider/provider.go

Learnt from: CR
Repo: tuannvm/oauth-mcp-proxy PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-13T17:03:53.298Z
Learning: Applies to cache.go : Token caching uses sync.RWMutex with 5-minute TTL and background cleanup via deleteExpiredToken() goroutine to avoid lock contention

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No, let's leave that decision for the maintainers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@drombosky, understood — I'll leave that to the maintainers. Thanks for the clarification!


🧠 Learnings used
Learnt from: CR
Repo: tuannvm/oauth-mcp-proxy PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-13T17:03:53.298Z
Learning: Providers implement TokenValidator interface and all token validation happens in provider/provider.go

Learnt from: CR
Repo: tuannvm/oauth-mcp-proxy PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-13T17:03:53.298Z
Learning: Applies to cache.go : Token caching uses sync.RWMutex with 5-minute TTL and background cleanup via deleteExpiredToken() goroutine to avoid lock contention

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
oauth.go (2)

129-134: Consider logging when rejecting tokens within the buffer window.

The validation-failure path (line 125) logs before returning, but the buffer-rejection path silently returns an error. This could make debugging harder when tokens are rejected for being too close to expiration.

💡 Suggested improvement
 if time.Now().After(expiry) {
+    s.logger.Info("Token rejected: expires within buffer (hash: %s...)", tokenHash[:16])
     return nil, fmt.Errorf("authentication failed: token expired or expiring too soon")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oauth.go` around lines 129 - 134, The token buffer-rejection branch silently
returns an error; update the code around the expiry check (use symbols expiry,
s.config, TokenExpiryBuffer, and time.Now()) to log a descriptive message
(including expiry, TokenExpiryBuffer and current time) before returning the
fmt.Errorf so token rejections within the buffer window are recorded for
debugging; keep the existing error return value but ensure the log uses the same
logger/context used elsewhere in this module.

129-131: The nil-config guard is appropriate for the deprecated OAuthMiddleware path only.

The guard at lines 129-131 is not inconsistent with other methods—it's intentionally protective of the only method called on nil-config Servers. The OAuthMiddleware legacy function (middleware.go:64-68) creates a Server without config and exclusively calls Middleware(), which in turn only calls ValidateTokenCached(). Methods like GetAuthorizationServerMetadataURL() are never invoked on nil-config Servers; they're only called on properly configured instances from the modern WithOAuth() path.

The design is safe as-is. However, if the codebase plans to support other methods being called on nil-config Servers, consider adding nil guards to all config-accessing methods for defensive programming. The current deprecation notice on OAuthMiddleware sufficiently documents this as a legacy-only pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oauth.go` around lines 129 - 131, The nil-config guard in the expiry
adjustment is acceptable because the legacy OAuthMiddleware path creates Servers
without config and only calls Middleware()→ValidateTokenCached(); leave the
existing guard in expiry calculation as-is, but if you intend to support calling
other methods on nil-config Servers in future, add consistent nil checks to all
config-accessing methods (e.g., GetAuthorizationServerMetadataURL, any methods
that read s.config) to early-return a clear error or no-op; locate and update
those methods (and constructors like WithOAuth and OAuthMiddleware) so they
either require non-nil config or defensively handle nil s.config.
api_test.go (1)

763-763: Ignored error from token signing.

The error from SignedString is discarded. While unlikely to fail in tests with a valid key, a signing failure would cause cryptic test failures downstream.

💡 Suggested fix
-			tokenString, _ := token.SignedString(cfg.JWTSecret)
+			tokenString, err := token.SignedString(cfg.JWTSecret)
+			if err != nil {
+				t.Fatalf("SignedString: %v", err)
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_test.go` at line 763, The call to token.SignedString(cfg.JWTSecret)
currently ignores the returned error; change it to capture both returns
(tokenString, err := token.SignedString(cfg.JWTSecret)), check err and fail the
test immediately (e.g., t.Fatalf or require.NoError) so a signing failure
surfaces clearly; update any surrounding test helper to propagate or handle the
error accordingly so tokenString is only used when signing succeeded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@api_test.go`:
- Line 763: The call to token.SignedString(cfg.JWTSecret) currently ignores the
returned error; change it to capture both returns (tokenString, err :=
token.SignedString(cfg.JWTSecret)), check err and fail the test immediately
(e.g., t.Fatalf or require.NoError) so a signing failure surfaces clearly;
update any surrounding test helper to propagate or handle the error accordingly
so tokenString is only used when signing succeeded.

In `@oauth.go`:
- Around line 129-134: The token buffer-rejection branch silently returns an
error; update the code around the expiry check (use symbols expiry, s.config,
TokenExpiryBuffer, and time.Now()) to log a descriptive message (including
expiry, TokenExpiryBuffer and current time) before returning the fmt.Errorf so
token rejections within the buffer window are recorded for debugging; keep the
existing error return value but ensure the log uses the same logger/context used
elsewhere in this module.
- Around line 129-131: The nil-config guard in the expiry adjustment is
acceptable because the legacy OAuthMiddleware path creates Servers without
config and only calls Middleware()→ValidateTokenCached(); leave the existing
guard in expiry calculation as-is, but if you intend to support calling other
methods on nil-config Servers in future, add consistent nil checks to all
config-accessing methods (e.g., GetAuthorizationServerMetadataURL, any methods
that read s.config) to early-return a clear error or no-op; locate and update
those methods (and constructors like WithOAuth and OAuthMiddleware) so they
either require non-nil config or defensively handle nil s.config.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04a076b0-d495-4f08-9007-6d4f6f7953d7

📥 Commits

Reviewing files that changed from the base of the PR and between 2471b78 and 8f76e17.

📒 Files selected for processing (9)
  • api_test.go
  • config.go
  • context_propagation_test.go
  • docs/SECURITY.md
  • integration_test.go
  • middleware.go
  • oauth.go
  • provider/provider.go
  • provider/provider_test.go
✅ Files skipped from review due to trivial changes (1)
  • docs/SECURITY.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • context_propagation_test.go
  • integration_test.go
  • middleware.go
  • provider/provider_test.go
  • provider/provider.go
  • config.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@oauth.go`:
- Around line 132-137: The current check uses time.Now().After(expiry) which
permits caching when expiry == now; change the conditional to be inclusive so
tokens with expiry equal to now are rejected (e.g., use if
!time.Now().Before(expiry) or if time.Now().After(expiry) ||
time.Now().Equal(expiry)) so the s.logger.Info message and return error happen
when expiry <= now, and only call s.cache.setCachedToken(tokenHash, user,
expiry) for tokens with strictly positive remaining lifetime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03b08b88-03b7-42e1-a493-405b67417720

📥 Commits

Reviewing files that changed from the base of the PR and between 8f76e17 and 7ee83f1.

📒 Files selected for processing (9)
  • api_test.go
  • config.go
  • context_propagation_test.go
  • docs/SECURITY.md
  • integration_test.go
  • middleware.go
  • oauth.go
  • provider/provider.go
  • provider/provider_test.go
✅ Files skipped from review due to trivial changes (2)
  • docs/SECURITY.md
  • integration_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • provider/provider_test.go
  • context_propagation_test.go
  • config.go
  • api_test.go
  • provider/provider.go

Comment thread oauth.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
middleware.go (1)

15-28: ⚠️ Potential issue | 🟡 Minor

Stale doc comment: "5-minute TTL" is no longer accurate.

The doc comment at line 20 still references a fixed "5-minute TTL", but the cache TTL is now derived from the JWT exp claim minus TokenExpiryBuffer.

📝 Proposed fix
 // Middleware returns an authentication middleware for MCP tools.
 // Validates OAuth tokens, caches results, and adds authenticated user to context.
 //
 // The middleware:
 //  1. Extracts OAuth token from context (set by CreateHTTPContextFunc)
-//  2. Checks token cache (5-minute TTL)
+//  2. Checks token cache (TTL derived from JWT exp minus TokenExpiryBuffer)
 //  3. Validates token using configured provider if not cached
 //  4. Adds User to context via userContextKey
 //  5. Passes request to tool handler with authenticated context
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware.go` around lines 15 - 28, Update the stale doc comment that says
"5-minute TTL" to describe the current behavior: the cache TTL is computed from
the JWT exp claim minus TokenExpiryBuffer (instead of a fixed 5 minutes).
Mention the relevant symbols so readers can find the implementation:
TokenExpiryBuffer, userContextKey, GetUserFromContext, WithOAuth, and
CreateHTTPContextFunc; keep the rest of the comment about extracting token,
checking cache, validating with provider, adding User to context, and passing to
handler unchanged.
context_propagation_test.go (1)

154-158: ⚠️ Potential issue | 🟡 Minor

Stale comment: signature documentation is outdated.

The comments at lines 157-158 still document the old signature ValidateToken(ctx context.Context, token string) (*User, error), but the method now returns (*User, time.Time, error).

📝 Proposed fix
 		// The key point: interface method signature
-		// ValidateToken(ctx context.Context, token string) (*User, error)
+		// ValidateToken(ctx context.Context, token string) (*User, time.Time, error)

 		t.Logf("✅ OIDCValidator implements TokenValidator with context.Context")
-		t.Logf("   Signature: ValidateToken(ctx context.Context, token string) (*User, error)")
+		t.Logf("   Signature: ValidateToken(ctx context.Context, token string) (*User, time.Time, error)")
 		t.Logf("   Context flow: HTTP → MCP → Middleware → ValidateToken(ctx) → OIDC Provider")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@context_propagation_test.go` around lines 154 - 158, Update the stale comment
in context_propagation_test.go that documents the ValidateToken signature:
change the old signature "ValidateToken(ctx context.Context, token string)
(*User, error)" to the current "ValidateToken(ctx context.Context, token string)
(*User, time.Time, error)" and ensure any accompanying descriptive text (e.g.,
context flow lines) remains accurate for the updated return values; locate the
comment near the tests referencing ValidateToken and adjust it to reflect the
new return types.
🧹 Nitpick comments (1)
config.go (1)

349-364: Consider adding TOKEN_EXPIRY_BUFFER environment variable support.

FromEnv() doesn't parse a TOKEN_EXPIRY_BUFFER (or similar) environment variable, so users relying on environment-based configuration cannot configure this feature without code changes.

💡 Proposed enhancement
+// parseDurationEnv parses a duration environment variable
+func parseDurationEnv(key string, defaultVal time.Duration) time.Duration {
+	val := getEnv(key, "")
+	if val == "" {
+		return defaultVal
+	}
+	parsed, err := time.ParseDuration(val)
+	if err != nil {
+		return defaultVal
+	}
+	return parsed
+}

Then in FromEnv():

 	return NewConfigBuilder().
 		// ... existing calls ...
 		WithValidatorIssuer(getEnv("OIDC_VALIDATOR_ISSUER", "")).
+		WithTokenExpiryBuffer(parseDurationEnv("TOKEN_EXPIRY_BUFFER", 0)).
 		WithServerURL(serverURL).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.go` around lines 349 - 364, FromEnv() doesn't read a
TOKEN_EXPIRY_BUFFER env var; update the env loading chain to parse that variable
and pass it into the config builder. Specifically, parse TOKEN_EXPIRY_BUFFER
(e.g. via a new or existing helper like parseDurationEnv("TOKEN_EXPIRY_BUFFER",
0) or parseIntEnv("TOKEN_EXPIRY_BUFFER", 0)) and add
.WithTokenExpiryBuffer(parsedValue) to the NewConfigBuilder() chain (and
implement WithTokenExpiryBuffer on the builder and the underlying Config struct
if missing) so environment-based configuration can control the token expiry
buffer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@context_propagation_test.go`:
- Around line 154-158: Update the stale comment in context_propagation_test.go
that documents the ValidateToken signature: change the old signature
"ValidateToken(ctx context.Context, token string) (*User, error)" to the current
"ValidateToken(ctx context.Context, token string) (*User, time.Time, error)" and
ensure any accompanying descriptive text (e.g., context flow lines) remains
accurate for the updated return values; locate the comment near the tests
referencing ValidateToken and adjust it to reflect the new return types.

In `@middleware.go`:
- Around line 15-28: Update the stale doc comment that says "5-minute TTL" to
describe the current behavior: the cache TTL is computed from the JWT exp claim
minus TokenExpiryBuffer (instead of a fixed 5 minutes). Mention the relevant
symbols so readers can find the implementation: TokenExpiryBuffer,
userContextKey, GetUserFromContext, WithOAuth, and CreateHTTPContextFunc; keep
the rest of the comment about extracting token, checking cache, validating with
provider, adding User to context, and passing to handler unchanged.

---

Nitpick comments:
In `@config.go`:
- Around line 349-364: FromEnv() doesn't read a TOKEN_EXPIRY_BUFFER env var;
update the env loading chain to parse that variable and pass it into the config
builder. Specifically, parse TOKEN_EXPIRY_BUFFER (e.g. via a new or existing
helper like parseDurationEnv("TOKEN_EXPIRY_BUFFER", 0) or
parseIntEnv("TOKEN_EXPIRY_BUFFER", 0)) and add
.WithTokenExpiryBuffer(parsedValue) to the NewConfigBuilder() chain (and
implement WithTokenExpiryBuffer on the builder and the underlying Config struct
if missing) so environment-based configuration can control the token expiry
buffer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83189017-8c6f-4d2c-9466-0c50721c46eb

📥 Commits

Reviewing files that changed from the base of the PR and between 7ee83f1 and 2e12ae1.

📒 Files selected for processing (9)
  • api_test.go
  • config.go
  • context_propagation_test.go
  • docs/SECURITY.md
  • integration_test.go
  • middleware.go
  • oauth.go
  • provider/provider.go
  • provider/provider_test.go
✅ Files skipped from review due to trivial changes (3)
  • integration_test.go
  • docs/SECURITY.md
  • provider/provider_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • provider/provider.go

Currently the code will cache a JWT for 5 minutes. This creates a timing
bug and forces unnecessary caching.

Since tokens are cached for 5 minutes, it is possible that a token with
1 second of expiration left is cached. The cache code pulls the token
out of the cache without any validation, the expired token will be used
for the next 4 minutes and 59 seconds.

This work sets the cache expiration time based on the token expiration
time. This results in each token being cached once and evicted from the
cache at expiration.

In cases where the JWT needs to be valid beyond this check (e.g., for
downstream requests), users can optionally choose to expire the cache
before the JWT expiration time with `TokenExpiryBuffer` option. This
option also ensures JWTs that are within the expiration window are
treated as expired.

Signed-off-by: Tyler Drombosky <bowsky@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant