Skip to content

Conversation

@KeeganBeuthin
Copy link
Contributor

Add support for configuring session cookie expiration based on access token claims. Sessions can now be configured as either persistent (29-day expiration) or session-only (browser close).

  • Add KSP claim extraction from JWT access tokens in TokenStore
  • Configure Rails session expiration via TokenManager
  • Default to persistent sessions for security when claims missing
  • Add session_persistent? utility method to KindeSdk module
  • Include comprehensive error handling with secure fallbacks

The implementation respects the ksp.persistence claim in access tokens, allowing dynamic session duration control per user session.

Add support for configuring session cookie expiration based on
access token claims. Sessions can now be configured as either
persistent (29-day expiration) or session-only (browser close).

- Add KSP claim extraction from JWT access tokens in TokenStore
- Configure Rails session expiration via TokenManager
- Default to persistent sessions for security when claims missing
- Add session_persistent? utility method to KindeSdk module
- Include comprehensive error handling with secure fallbacks

The implementation respects the ksp.persistence claim in access
tokens, allowing dynamic session duration control per user session.
@coderabbitai
Copy link

coderabbitai bot commented Sep 30, 2025

Walkthrough

Adds Kinde Session Persistence (KSP): TokenStore tracks and serializes a persistent flag (derived from tokens), TokenManager applies Rails session expiry based on that flag, KindeSdk applies persistence during client creation and exposes session_persistent?. Tests covering persistence cases were added.

Changes

Cohort / File(s) Summary of Changes
SDK Entry and Client Integration
lib/kinde_sdk.rb
Client factory now tolerates missing @config by using defaults per-call; client signature defaults to nil for options; applies TokenManager.apply_session_persistence when Current.token_store and Current.session exist; adds public KindeSdk.session_persistent?.
Session Management
lib/kinde_sdk/token_manager.rb
Adds apply_session_persistence(persistent, session) to set session.options[:expire_after] (29 days when persistent, nil otherwise) with rescue/logging; sync_session invokes this to align Rails session expiry.
Token Store and Serialization
lib/kinde_sdk/token_store.rb
Adds TWENTY_NINE_DAYS_SECONDS, @persistent flag (default true) and exposes :persistent; set_tokens extracts KSP persistence from access token; to_session/from_session include/restore persistent; adds cookie_expiration and private extract_ksp_persistence.
Specs / Tests
spec/kinde_sdk/ksp_spec.rb, spec/kinde_sdk_spec.rb, spec/examples.txt
Adds comprehensive KSP specs: persistent/non-persistent tokens, cookie expiration checks, session expire_after application, restoration from session data, malformed-token behavior; updates examples/test-results deletions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant KindeSdk
  participant Client
  participant Current
  participant TokenManager
  participant RailsSession as Rails Session

  App->>KindeSdk: client(tokens_hash, ...)
  KindeSdk->>Client: instantiate Client
  KindeSdk->>Current: read token_store, session
  alt Current has token_store and session
    KindeSdk->>TokenManager: apply_session_persistence(token_store.persistent, session)
    TokenManager->>RailsSession: set options[:expire_after] = 29d (if persistent) or nil
    TokenManager-->>KindeSdk: ok / rescued
  else Missing token_store or session
    KindeSdk-->>KindeSdk: skip persistence apply
  end
  KindeSdk-->>App: return Client
Loading
sequenceDiagram
  autonumber
  participant TokenStore
  participant AccessToken as Access Token (JWT)
  participant SessionData as Session Data

  Note over TokenStore: On set_tokens / initialize
  TokenStore->>AccessToken: inspect KSP claim
  TokenStore-->>TokenStore: persistent = extract_ksp_persistence(...) || true

  Note over TokenStore,SessionData: Persist/Restore
  TokenStore->>SessionData: to_session => includes persistent
  SessionData->>TokenStore: from_session(data)
  alt data includes persistent
    TokenStore-->>TokenStore: restore persistent from data
  else no persistent in data
    TokenStore->>AccessToken: derive persistence from token
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title clearly and concisely indicates the main new feature—session persistence control using JWT claims—directly reflecting the core of the changeset without unrelated details or vague language.
Description Check ✅ Passed The description accurately describes the purpose and content of the changeset, detailing JWT claim extraction, session expiration configuration, default behaviors, utility methods, and error handling, all of which align with the implemented code changes.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60d7ca1 and cc2613f.

📒 Files selected for processing (1)
  • spec/examples.txt (0 hunks)
💤 Files with no reviewable changes (1)
  • spec/examples.txt

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
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

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

Copy link

@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: 4

🧹 Nitpick comments (6)
lib/kinde_sdk/token_manager.rb (1)

90-96: Strengthen guard clause to handle nil session.options.

The guard at Line 91 checks session.options.is_a?(Hash), but if session.options returns nil, the check will pass respond_to?(:options) and then fail on .is_a?(Hash), potentially raising a NoMethodError before the rescue can catch it in some Ruby versions.

Apply this diff to ensure nil-safety:

-      return unless session.respond_to?(:options) && session.options.is_a?(Hash)
+      return unless session.respond_to?(:options) && session.options&.is_a?(Hash)
lib/kinde_sdk/token_store.rb (1)

35-43: Simplify from_session by using a single key check.

Lines 39-40 check both symbol and string keys (session_data.key?(:persistent) || session_data.key?('persistent')). Since set_tokens (Line 14) already transforms keys to symbols via transform_keys(&:to_sym), and to_session (Line 31) returns a hash with symbol keys, the string key check appears redundant.

Consider simplifying to:

-      if session_data.key?(:persistent) || session_data.key?('persistent')
-        store.instance_variable_set(:@persistent, session_data[:persistent] || session_data['persistent'])
+      if session_data.key?(:persistent)
+        store.instance_variable_set(:@persistent, session_data[:persistent])
       end

However, retaining both checks provides defense-in-depth if session data is deserialized from different sources (e.g., string-keyed session storage).

lib/kinde_sdk.rb (1)

126-126: Consider making apply_session_persistence public or using a documented pattern.

Line 126 uses TokenManager.send(:apply_session_persistence, ...) to call a private method. This bypasses Ruby's access control and makes the intent unclear.

Options:

  1. Make apply_session_persistence a public class method if it's part of the intended public API.
  2. Add a public wrapper method in TokenManager if the current design is intentional.
  3. Document why send() is necessary here.

If this is by design (e.g., to keep the method private from external callers but allow module-level access), add a comment:

 if Current.token_store && Current.session
+  # Call private method to apply session config (internal SDK use only)
   TokenManager.send(:apply_session_persistence, Current.token_store.persistent, Current.session)
 end
spec/kinde_sdk/ksp_spec.rb (3)

35-37: Suggest using constant for magic number.

The hardcoded value 2505600 appears in multiple places in the spec. Consider using KindeSdk::TokenStore::TWENTY_NINE_DAYS_SECONDS to improve maintainability and make the test's intent clearer.

Apply this diff:

-      it 'sets cookie expiration to 29 days' do
-        expect(store.cookie_expiration).to be_within(60).of(Time.now + 2505600)
-      end
+      it 'sets cookie expiration to 29 days' do
+        expect(store.cookie_expiration).to be_within(60).of(Time.now + KindeSdk::TokenStore::TWENTY_NINE_DAYS_SECONDS)
+      end

87-91: Use constant for magic number.

Same as previous comment - replace the hardcoded 2505600 with KindeSdk::TokenStore::TWENTY_NINE_DAYS_SECONDS.

Apply this diff:

       it 'sets expire_after to 29 days' do
-        expect(mock_session.options).to receive(:[]=).with(:expire_after, 2505600)
+        expect(mock_session.options).to receive(:[]=).with(:expire_after, KindeSdk::TokenStore::TWENTY_NINE_DAYS_SECONDS)
         
         token_manager.send(:apply_session_persistence, true, mock_session)
       end

202-209: Use constant for consistency.

Replace the hardcoded 2505600 with KindeSdk::TokenStore::TWENTY_NINE_DAYS_SECONDS for consistency with other tests and the implementation.

Apply this diff:

     it 'uses 29 days in seconds (2505600) matching other SDKs' do
       store = KindeSdk::TokenStore.new({ access_token: persistent_token })
-      expected_time = Time.now + 2505600
+      expected_time = Time.now + KindeSdk::TokenStore::TWENTY_NINE_DAYS_SECONDS
       
       expect(store.cookie_expiration).to be_within(1).of(expected_time)
     end
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59ac45d and cb967ca.

📒 Files selected for processing (4)
  • lib/kinde_sdk.rb (2 hunks)
  • lib/kinde_sdk/token_manager.rb (1 hunks)
  • lib/kinde_sdk/token_store.rb (1 hunks)
  • spec/kinde_sdk/ksp_spec.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
lib/kinde_sdk.rb (1)
lib/kinde_sdk/client.rb (1)
  • tokens_hash (49-51)
lib/kinde_sdk/token_store.rb (2)
lib/kinde_sdk/token_manager.rb (3)
  • attr_reader (2-98)
  • initialize (5-7)
  • set_tokens (9-14)
lib/kinde_sdk/client.rb (1)
  • initialize (31-39)
spec/kinde_sdk/ksp_spec.rb (2)
lib/kinde_sdk/token_store.rb (3)
  • cookie_expiration (48-50)
  • to_session (26-33)
  • from_session (35-43)
lib/kinde_sdk.rb (2)
  • session_persistent? (223-225)
  • client (120-130)
🪛 GitHub Actions: Ruby
lib/kinde_sdk.rb

[error] 120-120: NoMethodError: undefined method 'auto_refresh_tokens' for nil in KindeSdk.client.

spec/kinde_sdk/ksp_spec.rb

[error] 132-132: KindeSdk.session_persistent? with non-persistent token store returns false


[warning] 232-232: assigned but unused variable - new_expires_at


[warning] 270-270: assigned but unused variable - client_without_session

🔇 Additional comments (9)
lib/kinde_sdk/token_manager.rb (1)

82-85: LGTM!

The integration of session persistence application within sync_session is well-placed and correctly passes the store's persistent flag to configure the Rails session.

lib/kinde_sdk/token_store.rb (4)

3-4: Verify TWENTY_NINE_DAYS_SECONDS constant value.

The constant is set to 2505600 seconds. Let me verify this matches 29 days:

  • 29 days × 24 hours × 60 minutes × 60 seconds = 29 × 86400 = 2,505,600 ✓

The value is correct.


58-70: JWT.decode without signature verification is appropriate for claim extraction.

Line 61 calls JWT.decode(access_token, nil, false) with verification disabled. This is secure in this context because:

  • The token has already been validated via KindeSdk.validate_jwt_token before being stored
  • This extraction happens after authentication
  • Only reading a non-security-critical claim (ksp.persistence) that controls UX behavior, not authorization
  • Fails safely to true (persistent sessions) on any decode error

The implementation correctly matches the TypeScript SDK pattern and provides secure defaults.


17-22: LGTM! KSP persistence extraction is correctly integrated.

The extraction of the KSP claim from the access token is well-placed in set_tokens, ensuring persistence is updated whenever tokens are refreshed or set. The logic correctly handles the case when @bearer_token is present.


64-66: Verify ksp.persistence claim logic matches specification.

Line 66 implements: ksp_claim.fetch('persistence', true) != false

This returns true unless ksp.persistence is explicitly false. Confirm this matches the Kinde specification:

  • If ksp.persistence is missing → default true
  • If ksp.persistence is true → return true
  • If ksp.persistence is false → return false
  • If ksp.persistence is any other value → return true

The logic is correct and defensive.

spec/kinde_sdk/ksp_spec.rb (4)

1-24: LGTM! Clean test setup.

The token generation setup correctly creates test tokens with varying KSP claims. Using JWT.encode with a simple secret is appropriate for integration specs.


102-110: Good defensive test.

Testing graceful handling when session doesn't support options demonstrates good error resilience.


147-179: Excellent coverage of session restoration scenarios.

The tests comprehensively cover:

  • Symbol and string key variants (important for Rails session compatibility)
  • Fallback to token parsing when persistence is missing from session data
  • All restoration paths through from_session

181-200: LGTM! Integration test validates end-to-end flow.

This test correctly verifies that KSP configuration is automatically applied during client creation, validating the integration between KindeSdk.client, TokenManager.apply_session_persistence, and the token store.

Copy link

@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: 0

🧹 Nitpick comments (1)
lib/kinde_sdk/token_manager.rb (1)

87-96: Consider logging when session doesn't support options.

The method silently returns when session.respond_to?(:options) is false. While this defensive check is appropriate, consider logging a warning to help diagnose misconfiguration during development.

Apply this diff to add optional logging:

 def apply_session_persistence(persistent, session)
+  unless session.respond_to?(:options) && session.options.is_a?(Hash)
+    Rails.logger&.debug("Session object does not support options configuration")
+    return
+  end
-  return unless session.respond_to?(:options) && session.options.is_a?(Hash)
-  
   session.options[:expire_after] = persistent ? TokenStore::TWENTY_NINE_DAYS_SECONDS : nil
 rescue StandardError => e
   Rails.logger&.warn("KSP session configuration failed: #{e.message}")
 end
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb967ca and 60d7ca1.

📒 Files selected for processing (5)
  • lib/kinde_sdk.rb (3 hunks)
  • lib/kinde_sdk/token_manager.rb (1 hunks)
  • lib/kinde_sdk/token_store.rb (1 hunks)
  • spec/examples.txt (1 hunks)
  • spec/kinde_sdk_spec.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
lib/kinde_sdk/token_store.rb (2)
lib/kinde_sdk/token_manager.rb (3)
  • attr_reader (2-98)
  • initialize (5-7)
  • set_tokens (9-14)
lib/kinde_sdk/client.rb (1)
  • initialize (31-39)
spec/kinde_sdk_spec.rb (2)
lib/kinde_sdk.rb (2)
  • refresh_token (181-193)
  • client (120-135)
lib/kinde_sdk/current.rb (1)
  • clear_session (11-14)
lib/kinde_sdk.rb (1)
lib/kinde_sdk/configuration.rb (1)
  • default (41-43)
🔇 Additional comments (16)
lib/kinde_sdk/token_manager.rb (1)

82-85: LGTM!

The integration of apply_session_persistence into sync_session correctly applies KSP settings after syncing token data to the session.

spec/kinde_sdk_spec.rb (4)

274-308: LGTM!

The test correctly verifies that client creation applies session persistence when a persistent token is present. The use of send to invoke the private method is acceptable in specs to verify integration behavior.


310-330: LGTM!

The test correctly verifies that client creation applies session persistence with false when a non-persistent token is present.


332-343: LGTM!

The test correctly verifies that session persistence is skipped when no token store is present, exercising the guard condition in the client method.


345-359: LGTM!

The test correctly verifies that session persistence is skipped when no session is present, exercising the second guard condition in the client method.

lib/kinde_sdk.rb (3)

23-24: LGTM! Resolves NoMethodError issue.

Initializing @config to Configuration.default at module load prevents NoMethodError when client is called before configure. This addresses the pipeline failure from previous reviews.


120-134: LGTM! Past review concerns addressed.

The safe config fallback (Lines 121-124) prevents NoMethodError, and the automatic KSP application (Lines 129-132) is now covered by tests in spec/kinde_sdk_spec.rb (Lines 274-359).


225-231: LGTM! Past review concern resolved.

The explicit early return on Line 229 makes the default behavior clear: session is persistent when no token store exists (secure default), otherwise it reflects Current.token_store.persistent. This addresses the previous concern about || true always returning true.

lib/kinde_sdk/token_store.rb (8)

3-5: LGTM!

The 29-day constant is correctly calculated (29 × 86400 = 2,505,600 seconds) and appropriately scoped to the TokenStore class.


6-6: LGTM!

Adding :persistent to attr_reader correctly exposes the persistence state for use by TokenManager and KindeSdk.session_persistent?.


9-9: LGTM!

Defaulting to persistent sessions aligns with the secure default strategy mentioned in the PR description.


17-22: LGTM!

The conditional extraction of KSP persistence from the bearer token correctly updates the @persistent flag during set_tokens.


30-31: LGTM!

Including persistent in the session hash enables persistence state to be restored across requests.


37-43: LGTM!

The restoration logic correctly prioritizes persisted session state over re-extracting from the token, and defensively checks for both symbol and string keys.


45-50: Public utility method for cookie management.

The cookie_expiration method provides a clear API for determining session cookie expiration. While not currently used within the codebase, it's a valid public interface for applications to configure cookie persistence.


52-69: LGTM! Secure fallback strategy.

The KSP extraction correctly defaults to persistent sessions in all error and missing-claim cases, implementing a secure-by-default strategy. The logic ksp_claim.fetch('persistence', true) != false ensures only an explicit false value makes sessions non-persistent.

@onderay onderay requested a review from a team October 28, 2025 11:55
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