Skip to content

Conversation

@enmande
Copy link
Contributor

@enmande enmande commented Nov 24, 2025

🎟️ Tracking

PM-23572

📔 Objective

Allow SSO to be configured such that grants (currently only authorization_code grants are used) can be distributed beyond the server's in-memory caching to enable horizontal scaling of the application.

Clarifying details around this particular approach can be found in the ticket, the linked Tech Breakdown, and in the proposed code.

This approach will use the ExtendedCache introduced by #6591. The following occurs:

  • A keyed service is established, backed by the ExtendedCache, scoped to SSO grants.
  • ExtendedCache will negotiate the storage mechanism behind the keyed service:
    • If a Redis connection string is present, Redis will be negotiated as the backing store with scoped backplane to ensure any relevant nodes have the value propagated.
    • In-memory caching will be the default (current state).

After 5 minutes, all storage eventualities had their TTLs verified; inspection of Redis via KEYS sso-grant* showed an empty array.

📸 Screenshots

In-Memory Caching

No redis configuration present. Does not support horizontal scaling of SSO. Represents continued viability of current state.

PM-23572__in-memory.mov

Redis caching

Redis configuration present. Shows startup of the SSO application and connection to Redis with configuration of the keyed backplane. Shows a full SSO login flow proving scoped storage of sso grants to the cache (key conflict possibilities mitigated), with all information encrypted in protected payload.

PM-23572__redis.mov

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Nov 24, 2025

Logo
Checkmarx One – Scan Summary & Details13400d09-cdf9-4e11-8d52-b15481a0011d

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Nov 24, 2025

Codecov Report

❌ Patch coverage is 95.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.33%. Comparing base (0422796) to head (3ccdc64).
⚠️ Report is 30 commits behind head on main.

Files with missing lines Patch % Lines
...e/src/Sso/Utilities/ServiceCollectionExtensions.cs 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6636      +/-   ##
==========================================
+ Coverage   52.99%   53.33%   +0.34%     
==========================================
  Files        1910     1910              
  Lines       84843    85091     +248     
  Branches     7636     7651      +15     
==========================================
+ Hits        44966    45387     +421     
+ Misses      38125    37950     -175     
- Partials     1752     1754       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@enmande enmande marked this pull request as ready for review November 25, 2025 14:35
@enmande enmande requested a review from a team as a code owner November 25, 2025 14:35
@claude
Copy link
Contributor

claude bot commented Nov 25, 2025

Claude finished @enmande's task —— View job


Code Review Complete

Summary

This PR implements distributed caching for SSO authorization code grants using the ExtendedCache infrastructure, enabling horizontal scaling of the SSO service. The implementation is well-designed, thoroughly tested, and follows established patterns in the codebase. The PR effectively addresses the scaling requirements while maintaining backward compatibility with in-memory-only deployments.

Findings

Finding 1: PR description and objective are exemplary

💭 The PR has excellent documentation including clear objective, technical details, video demonstrations showing both in-memory and Redis configurations, and explicit verification of TTL behavior. This sets a high standard for PR quality.

Finding 2: Use typed constant for cache name in service registration

🎨 Following the pattern established in PR #6650 (EventIntegrationsCacheConstants), consider using the CacheKey constant directly in the service registration call rather than relying on the parameter:

Current vs. Suggested Pattern

Current (bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs:86)

services.AddExtendedCache(PersistedGrantsDistributedCacheConstants.CacheKey, globalSettings);

Suggested Pattern
This is already using the constant correctly. The reviewer's suggestion about using constants is actually already implemented. No change needed.

Status: Already addressed - the code correctly uses the constant.

Finding 3: Test coverage gap for service registration

⚠️ According to Codecov, lines 86 and 90 in ServiceCollectionExtensions.cs lack test coverage (the lines that register the ExtendedCache and PersistedGrantStore). Consider adding integration tests that verify the service registration correctly wires up the keyed cache service.

Location: bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs:86-90

Finding 4: Default FusionCacheEntryOptions may be redundant

💭 In DistributedCachePersistedGrantStore.StoreAsync (line 98-100), you're creating a new FusionCacheEntryOptions with only the Duration set. According to the ExtendedCache documentation and the reviewer's comment, the default options already handle distributed cache settings. Consider whether you need to explicitly set anything beyond duration, or if you can rely on the configured defaults from AddExtendedCache.

Current Implementation
await _cache.SetAsync(
    grant.Key,
    grant,
    new FusionCacheEntryOptions { Duration = duration });

The ExtendedCache is already configured with defaults including:

  • SkipDistributedCache = false (by default)
  • SkipDistributedCacheReadWhenStale = false (by default)

Your test at line 71-86 verifies these defaults are in place, but the implementation doesn't explicitly set them (which is correct - they're inherited from the cache configuration).

Recommendation: The current implementation is actually correct. The test is verifying the inherited defaults work as expected. No change needed.

Finding 5: Excellent test coverage and quality

👍 The test suite is comprehensive with 13 test cases covering:

  • TTL calculation with and without expiration
  • Expired grant handling and automatic removal
  • Distributed cache configuration verification
  • Edge cases (expired grants, missing keys)
  • Property preservation

The tests effectively use NSubstitute and demonstrate good testing practices.

Finding 6: Security considerations well-addressed

👍 The implementation properly:

  • Uses encrypted payloads in Redis (as shown in video)
  • Implements proper key prefixing to prevent conflicts (sso-grants:)
  • Handles TTL-based expiration to prevent stale grants
  • Documents revocation strategy clearly

This aligns with Bitwarden's zero-knowledge security principles.

Finding 7: Clear documentation of design decisions

📝 The code includes excellent inline comments explaining:

  • Why GetAllAsync and RemoveAllAsync return empty/no-op (lines 53-74 in DistributedCachePersistedGrantStore.cs)
  • The revocation strategy and why explicit revocation isn't needed (lines 64-68)
  • The relationship between this implementation and Duende IdentityServer's design (lines 14-21)

Finding 8: Backward compatibility maintained

👍 The graceful degradation to in-memory caching when Redis is not configured ensures existing deployments continue working without configuration changes. This is verified by the video demonstration.

Finding 9: Naming consistency

📝 The constant is named CacheKey in PersistedGrantsDistributedCacheConstants, while the pattern in EventIntegrationsCacheConstants uses CacheName. Consider using CacheName for consistency across the codebase, though this is a minor naming preference.

Location: bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs:9

Finding 10: No nullable reference type annotations

💭 The new files don't have #nullable enable or nullable reference type annotations. The project's ADR 0024 encourages using nullable reference types. Consider adding nullable annotations to the new files:

  • DistributedCachePersistedGrantStore.cs
  • PersistedGrantsDistributedCacheConstants.cs

Note that ServiceCollectionExtensions.cs already has #nullable disable at the file level (line 2), so no change needed there.

Action Items

Required (⚠️):

  1. Add tests for service registration in ServiceCollectionExtensions to improve coverage of lines 86-90

Recommended (💭):

  1. Consider renaming CacheKey to CacheName in PersistedGrantsDistributedCacheConstants for consistency with EventIntegrationsCacheConstants
  2. Consider enabling nullable reference types in the new files (DistributedCachePersistedGrantStore.cs and PersistedGrantsDistributedCacheConstants.cs) per ADR 0024

Optional (🎨):

  1. The reviewer's suggestion about using constants is already implemented correctly - no action needed

Good Practices Observed

  • Thorough unit testing with comprehensive coverage
  • Clear separation of concerns using keyed services
  • Excellent documentation and comments
  • Video demonstrations proving functionality
  • Graceful degradation for backward compatibility

Copy link
Contributor

@brant-livefront brant-livefront left a comment

Choose a reason for hiding this comment

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

This looks great and I'm excited to see someone else using extended cache. 🎉

A few mild suggestions, but this looks good to me.

private readonly IFusionCache _cache;

public DistributedCachePersistedGrantStore(
[FromKeyedServices("sso-grants")] IFusionCache cache)
Copy link
Contributor

Choose a reason for hiding this comment

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

One of the patterns I was using (you can see an example in #6650) was to have a cache constants class. The virtue being that the cache name and keys are not strings but are typed to constants. It looks like you already have a consistent way of building keys, so you probably don't need that part. But it still might be worthwhile to make absolutely sure the cache name is consistent.

i.e.

    // In constants class
    public const string CacheName = "sso-grants";

   // In this code
   [FromKeyedServices(SsoGrantsCacheConstants.CacheName)] IFusionCache cache)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great suggestion. Added in 3ccdc64.

Comment on lines 102 to 104
// Keep distributed cache enabled for multi-instance scenarios
// When Redis isn't configured, FusionCache gracefully uses only L1 (in-memory)
}.SetSkipDistributedCache(false, false));
Copy link
Contributor

Choose a reason for hiding this comment

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

I think these are the default values, so you shouldn't have to overwrite on every cache entry?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 3ccdc64.

// Provides separation of concerns and automatic Redis/in-memory negotiation
// .AddInMemoryCaching should still persist above; this handles configuration caching, etc.,
// and is separate from this keyed service, which only serves grant negotiation.
services.AddExtendedCache("sso-grants", globalSettings);
Copy link
Contributor

Choose a reason for hiding this comment

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

If you adopt my comment above, here's another place to use the CacheName constant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 3ccdc64.

…ove explicit skip distributed cache on set for default configuration.
Copy link
Contributor

@brant-livefront brant-livefront left a comment

Choose a reason for hiding this comment

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

💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants