diff --git a/bitwarden_license/src/Sso/IdentityServer/DistributedCachePersistedGrantStore.cs b/bitwarden_license/src/Sso/IdentityServer/DistributedCachePersistedGrantStore.cs
deleted file mode 100644
index ecb2f36cec..0000000000
--- a/bitwarden_license/src/Sso/IdentityServer/DistributedCachePersistedGrantStore.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using Bit.Sso.Utilities;
-using Duende.IdentityServer.Models;
-using Duende.IdentityServer.Stores;
-using ZiggyCreatures.Caching.Fusion;
-
-namespace Bit.Sso.IdentityServer;
-
-///
-/// Distributed cache-backed persisted grant store for short-lived grants.
-/// Uses IFusionCache (which wraps IDistributedCache) for horizontal scaling support,
-/// and fall back to in-memory caching if Redis is not configured.
-/// Designed for SSO authorization codes which are short-lived (5 minutes) and single-use.
-///
-///
-/// This is purposefully a different implementation from how Identity solves Persisted Grants.
-/// Because even flavored grant store, e.g., AuthorizationCodeGrantStore, can add intermediary
-/// logic to a grant's handling by type, the fact that they all wrap IdentityServer's IPersistedGrantStore
-/// leans on IdentityServer's opinion that all grants, regardless of type, go to the same persistence
-/// mechanism (cache, database).
-///
-///
-public class DistributedCachePersistedGrantStore : IPersistedGrantStore
-{
- private readonly IFusionCache _cache;
-
- public DistributedCachePersistedGrantStore(
- [FromKeyedServices(PersistedGrantsDistributedCacheConstants.CacheKey)] IFusionCache cache)
- {
- _cache = cache;
- }
-
- public async Task GetAsync(string key)
- {
- var result = await _cache.TryGetAsync(key);
-
- if (!result.HasValue)
- {
- return null;
- }
-
- var grant = result.Value;
-
- // Check if grant has expired - remove expired grants from cache
- if (grant.Expiration.HasValue && grant.Expiration.Value < DateTime.UtcNow)
- {
- await RemoveAsync(key);
- return null;
- }
-
- return grant;
- }
-
- public Task> GetAllAsync(PersistedGrantFilter filter)
- {
- // Cache stores are key-value based and don't support querying by filter criteria.
- // This method is typically used for cleanup operations on long-lived grants in databases.
- // For SSO's short-lived authorization codes, we rely on TTL expiration instead.
-
- return Task.FromResult(Enumerable.Empty());
- }
-
- public Task RemoveAllAsync(PersistedGrantFilter filter)
- {
- // Revocation Strategy: SSO's logout flow (AccountController.LogoutAsync) only clears local
- // authentication cookies and performs federated logout with external IdPs. It does not invoke
- // Duende's EndSession or TokenRevocation endpoints. Authorization codes are single-use and expire
- // within 5 minutes, making explicit revocation unnecessary for SSO's security model.
- // https://docs.duendesoftware.com/identityserver/reference/stores/persisted-grant-store/
-
- // Cache stores are key-value based and don't support bulk deletion by filter.
- // This method is typically used for cleanup operations on long-lived grants in databases.
- // For SSO's short-lived authorization codes, we rely on TTL expiration instead.
-
- return Task.FromResult(0);
- }
-
- public async Task RemoveAsync(string key)
- {
- await _cache.RemoveAsync(key);
- }
-
- public async Task StoreAsync(PersistedGrant grant)
- {
- // Calculate TTL based on grant expiration
- var duration = grant.Expiration.HasValue
- ? grant.Expiration.Value - DateTime.UtcNow
- : TimeSpan.FromMinutes(5); // Default to 5 minutes if no expiration set
-
- // Ensure positive duration
- if (duration <= TimeSpan.Zero)
- {
- return;
- }
-
- // Cache key "sso-grants:" is configured by service registration. Going through the consumed KeyedService will
- // give us a consistent cache key prefix for these grants.
- await _cache.SetAsync(
- grant.Key,
- grant,
- new FusionCacheEntryOptions { Duration = duration });
- }
-}
diff --git a/bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs b/bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs
deleted file mode 100644
index 3ec45377e3..0000000000
--- a/bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Bit.Sso.Utilities;
-
-public static class PersistedGrantsDistributedCacheConstants
-{
- ///
- /// The SSO Persisted Grant cache key. Identifies the keyed service consumed by the SSO Persisted Grant Store as
- /// well as the cache key/namespace for grant storage.
- ///
- public const string CacheKey = "sso-grants";
-}
diff --git a/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs
index da7a79535e..a51a04f5c8 100644
--- a/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs
+++ b/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs
@@ -9,7 +9,6 @@ using Bit.Sso.IdentityServer;
using Bit.Sso.Models;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.ResponseHandling;
-using Duende.IdentityServer.Stores;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Sustainsys.Saml2.AspNetCore2;
@@ -78,17 +77,6 @@ public static class ServiceCollectionExtensions
})
.AddIdentityServerCertificate(env, globalSettings);
- // PM-23572
- // Register named FusionCache for SSO authorization code grants.
- // 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(PersistedGrantsDistributedCacheConstants.CacheKey, globalSettings);
-
- // Store authorization codes in distributed cache for horizontal scaling
- // Uses named FusionCache which gracefully degrades to in-memory when Redis isn't configured
- services.AddSingleton();
-
return identityServerBuilder;
}
}
diff --git a/bitwarden_license/test/SSO.Test/IdentityServer/DistributedCachePersistedGrantStoreTests.cs b/bitwarden_license/test/SSO.Test/IdentityServer/DistributedCachePersistedGrantStoreTests.cs
deleted file mode 100644
index c0aa93f068..0000000000
--- a/bitwarden_license/test/SSO.Test/IdentityServer/DistributedCachePersistedGrantStoreTests.cs
+++ /dev/null
@@ -1,257 +0,0 @@
-using Bit.Sso.IdentityServer;
-using Duende.IdentityServer.Models;
-using Duende.IdentityServer.Stores;
-using NSubstitute;
-using ZiggyCreatures.Caching.Fusion;
-
-namespace Bit.SSO.Test.IdentityServer;
-
-public class DistributedCachePersistedGrantStoreTests
-{
- private readonly IFusionCache _cache;
- private readonly DistributedCachePersistedGrantStore _sut;
-
- public DistributedCachePersistedGrantStoreTests()
- {
- _cache = Substitute.For();
- _sut = new DistributedCachePersistedGrantStore(_cache);
- }
-
- [Fact]
- public async Task StoreAsync_StoresGrantWithCalculatedTTL()
- {
- // Arrange
- var grant = CreateTestGrant("test-key", expiration: DateTime.UtcNow.AddMinutes(5));
-
- // Act
- await _sut.StoreAsync(grant);
-
- // Assert
- await _cache.Received(1).SetAsync(
- "test-key",
- grant,
- Arg.Is(opts =>
- opts.Duration >= TimeSpan.FromMinutes(4.9) &&
- opts.Duration <= TimeSpan.FromMinutes(5)));
- }
-
- [Fact]
- public async Task StoreAsync_WithNoExpiration_UsesDefaultFiveMinuteTTL()
- {
- // Arrange
- var grant = CreateTestGrant("no-expiry-key", expiration: null);
-
- // Act
- await _sut.StoreAsync(grant);
-
- // Assert
- await _cache.Received(1).SetAsync(
- "no-expiry-key",
- grant,
- Arg.Is(opts => opts.Duration == TimeSpan.FromMinutes(5)));
- }
-
- [Fact]
- public async Task StoreAsync_WithAlreadyExpiredGrant_DoesNotStore()
- {
- // Arrange
- var expiredGrant = CreateTestGrant("expired-key", expiration: DateTime.UtcNow.AddMinutes(-1));
-
- // Act
- await _sut.StoreAsync(expiredGrant);
-
- // Assert
- await _cache.DidNotReceive().SetAsync(
- Arg.Any(),
- Arg.Any(),
- Arg.Any());
- }
-
- [Fact]
- public async Task StoreAsync_EnablesDistributedCache()
- {
- // Arrange
- var grant = CreateTestGrant("distributed-key", expiration: DateTime.UtcNow.AddMinutes(5));
-
- // Act
- await _sut.StoreAsync(grant);
-
- // Assert
- await _cache.Received(1).SetAsync(
- "distributed-key",
- grant,
- Arg.Is(opts =>
- opts.SkipDistributedCache == false &&
- opts.SkipDistributedCacheReadWhenStale == false));
- }
-
- [Fact]
- public async Task GetAsync_WithValidGrant_ReturnsGrant()
- {
- // Arrange
- var grant = CreateTestGrant("valid-key", expiration: DateTime.UtcNow.AddMinutes(5));
- _cache.TryGetAsync("valid-key")
- .Returns(MaybeValue.FromValue(grant));
-
- // Act
- var result = await _sut.GetAsync("valid-key");
-
- // Assert
- Assert.NotNull(result);
- Assert.Equal("valid-key", result.Key);
- Assert.Equal("authorization_code", result.Type);
- Assert.Equal("test-subject", result.SubjectId);
- await _cache.DidNotReceive().RemoveAsync(Arg.Any());
- }
-
- [Fact]
- public async Task GetAsync_WithNonExistentKey_ReturnsNull()
- {
- // Arrange
- _cache.TryGetAsync("nonexistent-key")
- .Returns(MaybeValue.None);
-
- // Act
- var result = await _sut.GetAsync("nonexistent-key");
-
- // Assert
- Assert.Null(result);
- await _cache.DidNotReceive().RemoveAsync(Arg.Any());
- }
-
- [Fact]
- public async Task GetAsync_WithExpiredGrant_RemovesAndReturnsNull()
- {
- // Arrange
- var expiredGrant = CreateTestGrant("expired-key", expiration: DateTime.UtcNow.AddMinutes(-1));
- _cache.TryGetAsync("expired-key")
- .Returns(MaybeValue.FromValue(expiredGrant));
-
- // Act
- var result = await _sut.GetAsync("expired-key");
-
- // Assert
- Assert.Null(result);
- await _cache.Received(1).RemoveAsync("expired-key");
- }
-
- [Fact]
- public async Task GetAsync_WithNoExpiration_ReturnsGrant()
- {
- // Arrange
- var grant = CreateTestGrant("no-expiry-key", expiration: null);
- _cache.TryGetAsync("no-expiry-key")
- .Returns(MaybeValue.FromValue(grant));
-
- // Act
- var result = await _sut.GetAsync("no-expiry-key");
-
- // Assert
- Assert.NotNull(result);
- Assert.Equal("no-expiry-key", result.Key);
- Assert.Null(result.Expiration);
- await _cache.DidNotReceive().RemoveAsync(Arg.Any());
- }
-
- [Fact]
- public async Task RemoveAsync_RemovesGrantFromCache()
- {
- // Act
- await _sut.RemoveAsync("remove-key");
-
- // Assert
- await _cache.Received(1).RemoveAsync("remove-key");
- }
-
- [Fact]
- public async Task GetAllAsync_ReturnsEmptyCollection()
- {
- // Arrange
- var filter = new PersistedGrantFilter
- {
- SubjectId = "test-subject",
- SessionId = "test-session",
- ClientId = "test-client",
- Type = "authorization_code"
- };
-
- // Act
- var result = await _sut.GetAllAsync(filter);
-
- // Assert
- Assert.NotNull(result);
- Assert.Empty(result);
- }
-
- [Fact]
- public async Task RemoveAllAsync_CompletesWithoutError()
- {
- // Arrange
- var filter = new PersistedGrantFilter
- {
- SubjectId = "test-subject",
- ClientId = "test-client"
- };
-
- // Act & Assert - should not throw
- await _sut.RemoveAllAsync(filter);
-
- // Verify no cache operations were performed
- await _cache.DidNotReceive().RemoveAsync(Arg.Any());
- }
-
- [Fact]
- public async Task StoreAsync_PreservesAllGrantProperties()
- {
- // Arrange
- var grant = new PersistedGrant
- {
- Key = "full-grant-key",
- Type = "authorization_code",
- SubjectId = "user-123",
- SessionId = "session-456",
- ClientId = "client-789",
- Description = "Test grant",
- CreationTime = DateTime.UtcNow.AddMinutes(-1),
- Expiration = DateTime.UtcNow.AddMinutes(5),
- ConsumedTime = null,
- Data = "{\"test\":\"data\"}"
- };
-
- PersistedGrant? capturedGrant = null;
- await _cache.SetAsync(
- Arg.Any(),
- Arg.Do(g => capturedGrant = g),
- Arg.Any());
-
- // Act
- await _sut.StoreAsync(grant);
-
- // Assert
- Assert.NotNull(capturedGrant);
- Assert.Equal(grant.Key, capturedGrant.Key);
- Assert.Equal(grant.Type, capturedGrant.Type);
- Assert.Equal(grant.SubjectId, capturedGrant.SubjectId);
- Assert.Equal(grant.SessionId, capturedGrant.SessionId);
- Assert.Equal(grant.ClientId, capturedGrant.ClientId);
- Assert.Equal(grant.Description, capturedGrant.Description);
- Assert.Equal(grant.CreationTime, capturedGrant.CreationTime);
- Assert.Equal(grant.Expiration, capturedGrant.Expiration);
- Assert.Equal(grant.ConsumedTime, capturedGrant.ConsumedTime);
- Assert.Equal(grant.Data, capturedGrant.Data);
- }
-
- private static PersistedGrant CreateTestGrant(string key, DateTime? expiration)
- {
- return new PersistedGrant
- {
- Key = key,
- Type = "authorization_code",
- SubjectId = "test-subject",
- ClientId = "test-client",
- CreationTime = DateTime.UtcNow,
- Expiration = expiration,
- Data = "{\"test\":\"data\"}"
- };
- }
-}