diff --git a/Directory.Build.props b/Directory.Build.props index d0e6b43fc..f34316f4f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 3.1.2 + 3.1.3 3.1 RockSolidKnowledge CS1591;CS1570;CS1571;CS1572;CS1573;CS1574;CS1580;CS1581;CS1584;CS1587;CS1591;CS1658;CS1712;CS1734 diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 02691d1e4..1c63a59e9 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -5,7 +5,7 @@ 10.0.14 10.0.14 - 3.1.2-* + 3.1.3-* diff --git a/src/EntityFramework.Storage/src/Mappers/IdentityServerServerSideSessionsExtensions.cs b/src/EntityFramework.Storage/src/Mappers/IdentityServerServerSideSessionsExtensions.cs index fe20c9440..326aa7865 100644 --- a/src/EntityFramework.Storage/src/Mappers/IdentityServerServerSideSessionsExtensions.cs +++ b/src/EntityFramework.Storage/src/Mappers/IdentityServerServerSideSessionsExtensions.cs @@ -3,8 +3,10 @@ #nullable enable +using System; using Open.IdentityServer.EntityFramework.Entities; using Open.IdentityServer.Models; +using Open.IdentityServer.Stores.Serialization; namespace Open.IdentityServer.EntityFramework.Mappers; @@ -23,17 +25,19 @@ public static class IdentityServerServerSideSessionsExtensions /// Mapper for to convert into an instance of /// /// mapped instance of - public Models.ServerSideSession ToModel() => - sessionEntity.ToModel(); + public Models.ServerSideSession ToModel(IAuthenticationTicketSerializer authTicketSerializer) => + sessionEntity.ToModel(authTicketSerializer); /// /// Mapper for to convert into an instance of . /// /// The type of model to map to. /// mapped instance of . - public TModel ToModel() + public TModel ToModel(IAuthenticationTicketSerializer authTicketSerializer) where TModel: Models.ServerSideSession, new() { + var authenticationTicket = authTicketSerializer.Deserialize(sessionEntity.Data); + return new TModel { Key = sessionEntity.Key, @@ -44,7 +48,8 @@ public TModel ToModel() Created = sessionEntity.Created, Renewed = sessionEntity.Renewed, Expires = sessionEntity.Expires, - Data = sessionEntity.Data + Data = sessionEntity.Data, + AuthenticationTicket = authenticationTicket, }; } } @@ -58,10 +63,11 @@ public TModel ToModel() /// /// Mapper for to convert into an instance of /// + /// authentication ticket serializer /// mapped instance of - public Entities.ServerSideSession ToEntity() + public Entities.ServerSideSession ToEntity(IAuthenticationTicketSerializer authTicketSerializer) { - return new Entities.ServerSideSession + Entities.ServerSideSession newEntity = new() { Key = sessionModel.Key, Scheme = sessionModel.Scheme, @@ -71,15 +77,22 @@ public Entities.ServerSideSession ToEntity() Created = sessionModel.Created, Renewed = sessionModel.Renewed, Expires = sessionModel.Expires, - Data = sessionModel.Data }; + + if (sessionModel.AuthenticationTicket != null) + { + newEntity.Data = authTicketSerializer.Serialize(sessionModel.AuthenticationTicket); + } + + return newEntity; } - + /// /// Updates with instance of /// + /// authentication ticket serializer /// The entity. - public void UpdateEntity(Entities.ServerSideSession existingEntity) + public void UpdateEntity(IAuthenticationTicketSerializer authTicketSerializer, Entities.ServerSideSession existingEntity) { existingEntity.Key = sessionModel.Key; existingEntity.Scheme = sessionModel.Scheme; @@ -89,7 +102,11 @@ public void UpdateEntity(Entities.ServerSideSession existingEntity) existingEntity.Created = sessionModel.Created; existingEntity.Renewed = sessionModel.Renewed; existingEntity.Expires = sessionModel.Expires; - existingEntity.Data = sessionModel.Data; + + if (sessionModel.AuthenticationTicket != null) + { + existingEntity.Data = authTicketSerializer.Serialize(sessionModel.AuthenticationTicket); + } } } } \ No newline at end of file diff --git a/src/EntityFramework.Storage/src/Stores/IdentityServerServerSideSessionStore.cs b/src/EntityFramework.Storage/src/Stores/IdentityServerServerSideSessionStore.cs index 2f8198624..653ec2e2b 100644 --- a/src/EntityFramework.Storage/src/Stores/IdentityServerServerSideSessionStore.cs +++ b/src/EntityFramework.Storage/src/Stores/IdentityServerServerSideSessionStore.cs @@ -15,6 +15,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Services; using Open.IdentityServer.Stores; +using Open.IdentityServer.Stores.Serialization; namespace Open.IdentityServer.EntityFramework.Stores; @@ -23,6 +24,7 @@ namespace Open.IdentityServer.EntityFramework.Stores; /// public class IdentityServerServerSideSessionStore( IPersistedGrantDbContext dbContext, + IAuthenticationTicketSerializer authTicketSerializer, ITelemetryService telemetry, TimeProvider timeProvider, ILogger logger) : IIdentityServerServerSideSessionStore @@ -35,8 +37,12 @@ public class IdentityServerServerSideSessionStore( /// /// Makes it possible to return an extended model. /// - protected virtual ServerSideSession? ToModel(Entities.ServerSideSession? entities) => - entities?.ToModel(); + protected virtual ServerSideSession? ToModel(Entities.ServerSideSession? entities) + { + var result = entities?.ToModel(authTicketSerializer); + + return result; + } /// public async Task GetSession(string key) @@ -68,7 +74,7 @@ public async Task CreateSession(ServerSideSession session) return; } - Entities.ServerSideSession sessionEntity = session.ToEntity(); + Entities.ServerSideSession sessionEntity = session.ToEntity(authTicketSerializer); await dbContext.ServerSideSessions.AddAsync(sessionEntity); @@ -98,7 +104,7 @@ public async Task UpdateSession(ServerSideSession session) return; } - session.UpdateEntity(existing); + session.UpdateEntity(authTicketSerializer, existing); try { diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/ExtendedIdentityServerServerSideSessionStore.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/ExtendedIdentityServerServerSideSessionStore.cs index 91b5f6c96..45d8c2d13 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/ExtendedIdentityServerServerSideSessionStore.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/ExtendedIdentityServerServerSideSessionStore.cs @@ -4,19 +4,21 @@ using Open.IdentityServer.EntityFramework.Mappers; using Open.IdentityServer.EntityFramework.Stores; using Open.IdentityServer.Services; +using Open.IdentityServer.Stores.Serialization; namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores.Compatibility; public class ExtendedIdentityServerServerSideSessionStore( IPersistedGrantDbContext dbContext, + IAuthenticationTicketSerializer authTicketSerializer, ITelemetryService telemetry, TimeProvider timeProvider, ILogger logger): - IdentityServerServerSideSessionStore(dbContext, telemetry, timeProvider, logger) + IdentityServerServerSideSessionStore(dbContext, authTicketSerializer, telemetry, timeProvider, logger) { protected override Models.ServerSideSession ToModel(Entities.ServerSideSession client) { - var model = client.ToModel(); + var model = client.ToModel(authTicketSerializer); model.DataLength = model.Data.Length; model.Lifetime = model.Expires?.Subtract(model.Renewed); diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/IdentityServerServerSideSessionStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/IdentityServerServerSideSessionStoreTests.cs index 85d3bb477..2cf0a082f 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/IdentityServerServerSideSessionStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/Compatibility/IdentityServerServerSideSessionStoreTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; +using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; @@ -12,6 +13,8 @@ using Open.IdentityServer.EntityFramework.Stores; using Open.IdentityServer.Models; using Open.IdentityServer.Services; +using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.Test.Utilities; using Open.IdentityServer.Test.Utilities; using Xunit; @@ -19,6 +22,7 @@ namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores.Compatibil public class IdentityServerServerSideSessionStoreTests: IntegrationTest { + private readonly IAuthenticationTicketSerializer authTicketSerializer = Mock.Of(); private readonly ITelemetryService telemetry = Mock.Of(); private readonly FakeTimeProvider timeProvider = new(); private readonly MockLogger fakeLogger = new(); @@ -37,10 +41,10 @@ public IdentityServerServerSideSessionStoreTests(DatabaseProviderFixture - new(dbContext, telemetry, timeProvider, fakeLogger); + new(dbContext, authTicketSerializer, telemetry, timeProvider, fakeLogger); private ExtendedIdentityServerServerSideSessionStore CreateSutExtended(PersistedGrantDbContext dbContext) => - new (dbContext, telemetry, timeProvider, fakeLogger); + new (dbContext, authTicketSerializer, telemetry, timeProvider, fakeLogger); [Theory] [InlineData(null)] @@ -83,8 +87,13 @@ public async Task GetSession_WhenExist_ShouldReturnValue(DbContextOptions x.Deserialize(seeded.Data)) + .Returns(fakeTicket); context.ServerSideSessions.Add(seeded); await context.SaveChangesAsync(); @@ -103,6 +112,7 @@ public async Task GetSession_WhenExist_ShouldReturnValue(DbContextOptions x.Deserialize(seeded.Data)) + .Returns(fakeTicket); context.ServerSideSessions.Add(seeded); await context.SaveChangesAsync(); @@ -144,6 +159,7 @@ public async Task GetSession_WhenExist_UsingExtendedStore_ShouldReturnValueOfTyp resultExt.Renewed.Should().Be(seeded.Renewed); resultExt.Expires.Should().Be(seeded.Expires); resultExt.Data.Should().Be(seeded.Data); + result.AuthenticationTicket.Should().Be(fakeTicket); resultExt.DataLength.Should().Be(seeded.Data.Length); resultExt.Lifetime.Should().Be(seeded.Expires?.Subtract(seeded.Renewed)); @@ -199,26 +215,31 @@ public async Task CreateSession_WhenSessionDoesntExistsWithKey_ShouldStoreSessio await using PersistedGrantDbContext context = await CreateCleanContext(options); string key = "new-key"; - ServerSideSession session = BuildSessionModel(key, "sub-123", "sid-123", "display-123"); + ServerSideSession newSession = BuildSessionModel(key, "sub-123", "sid-123", "display-123"); + + const string fakeProtectedData = "{\"version\":1,\"payload\":\"PROTECTED_DATA\"}"; + Mock.Get(authTicketSerializer) + .Setup(x => x.Serialize(newSession.AuthenticationTicket)) + .Returns(fakeProtectedData); IdentityServerServerSideSessionStore sut = CreateSut(context); - await sut.CreateSession(session); + await sut.CreateSession(newSession); Entities.ServerSideSession stored = await context.ServerSideSessions .AsNoTracking() .SingleOrDefaultAsync(x => x.Key == key, cancellationToken: TestContext.Current.CancellationToken); stored.Should().NotBeNull(); - stored!.Key.Should().Be(session.Key); - stored.Scheme.Should().Be(session.Scheme); - stored.SubjectId.Should().Be(session.SubjectId); - stored.SessionId.Should().Be(session.SessionId); - stored.DisplayName.Should().Be(session.DisplayName); - stored.Created.Should().Be(session.Created); - stored.Renewed.Should().Be(session.Renewed); - stored.Expires.Should().Be(session.Expires); - stored.Data.Should().Be(session.Data); + stored!.Key.Should().Be(newSession.Key); + stored.Scheme.Should().Be(newSession.Scheme); + stored.SubjectId.Should().Be(newSession.SubjectId); + stored.SessionId.Should().Be(newSession.SessionId); + stored.DisplayName.Should().Be(newSession.DisplayName); + stored.Created.Should().Be(newSession.Created); + stored.Renewed.Should().Be(newSession.Renewed); + stored.Expires.Should().Be(newSession.Expires); + stored.Data.Should().Be(fakeProtectedData); } [Theory] @@ -272,10 +293,14 @@ public async Task UpdateSession_WhenSessionExistsWithKey_ShouldUpdateStoredSessi ServerSideSession updated = BuildSessionModel(key, "new-sub", "new-sid", "new-display"); updated.Scheme = "new-scheme"; - updated.Data = "{\"version\":2}"; updated.Created = FakeNow.AddHours(-3); updated.Renewed = FakeNow.AddMinutes(-1); updated.Expires = FakeNow.AddHours(2); + + const string fakeProtectedData = "{\"version\":1,\"payload\":\"PROTECTED_DATA\"}"; + Mock.Get(authTicketSerializer) + .Setup(x => x.Serialize(updated.AuthenticationTicket)) + .Returns(fakeProtectedData); IdentityServerServerSideSessionStore sut = CreateSut(context); @@ -293,7 +318,7 @@ public async Task UpdateSession_WhenSessionExistsWithKey_ShouldUpdateStoredSessi stored.Created.Should().Be(updated.Created); stored.Renewed.Should().Be(updated.Renewed); stored.Expires.Should().Be(updated.Expires); - stored.Data.Should().Be(updated.Data); + stored.Data.Should().Be(fakeProtectedData); } [Theory] @@ -518,22 +543,44 @@ public async Task FilterSessions_WhenSubjectIdNull_ShouldReturnMatchingSessionId await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + foundSessionOne, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + foundSessionTwo, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); + + List expectedSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); + await context.SaveChangesAsync(); var actual = (await sut.FilterSessions(null,"session-0")).ToList(); - actual.Should().HaveCount(2); - actual.Should().Contain(x => x.Key == "key-0"); - actual.Should().Contain(x => x.Key == "key-4"); + actual.Should().BeEquivalentTo(expectedSessions); + } + + private List SetupExpectedSessions(List sessionEntities) + { + List expectedSessions = []; + + foreach (var sessionEntity in sessionEntities) + { + var session = BuildSessionModel(sessionEntity.Key, sessionEntity.SubjectId, sessionEntity.SessionId, sessionEntity.DisplayName, scheme: sessionEntity.Scheme, created: sessionEntity.Created, renewed: sessionEntity.Renewed, expires: sessionEntity.Expires, data: sessionEntity.Data); + + Mock.Get(authTicketSerializer) + .Setup(x => x.Deserialize(sessionEntity.Data)) + .Returns(session.AuthenticationTicket); + + expectedSessions.Add(session); + } + + return expectedSessions; } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -543,13 +590,13 @@ public async Task FilterSessions_WhenSessionDontMatch_ShouldReturnEmptySet(DbCon IdentityServerServerSideSessionStore sut = CreateSut(context); await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); @@ -563,47 +610,53 @@ public async Task FilterSessions_WhenSessionMatch_ShouldReturnMatchingSessions(D { await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + + Entities.ServerSideSession foundSessionOne = new () { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }; await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + foundSessionOne, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + foundSessionTwo, ]); await context.SaveChangesAsync(); + List expectedSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); + var actual = (await sut.FilterSessions("alice", "session-1")).ToList(); - actual.Should().HaveCount(2); - actual.Should().Contain(x => x.Key == "key-1"); - actual.Should().Contain(x => x.Key == "key-6"); + actual.Should().BeEquivalentTo(expectedSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] - public async Task FilterSessions_WhenExtendedStire_ShouldReturnMatchingExtendedSessionModels(DbContextOptions options) + public async Task FilterSessions_WhenExtendedStore_ShouldReturnMatchingExtendedSessionModels(DbContextOptions options) { await using var context = await CreateCleanContext(options); ExtendedIdentityServerServerSideSessionStore sut = CreateSutExtended(context); + + Entities.ServerSideSession foundSessionOne = new () { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }; await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + foundSessionOne, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + foundSessionTwo, ]); await context.SaveChangesAsync(); + List expectedSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); + var actual = (await sut.FilterSessions("alice", "session-1")).ToList(); - actual.Should().HaveCount(2); - actual.Should().Contain(x => x.Key == "key-1"); - actual.Should().Contain(x => x.Key == "key-6"); + actual.Should().BeEquivalentTo(expectedSessions); foreach (var session in actual) { @@ -645,13 +698,13 @@ public async Task GetAndRemoveExpiredSessions_WhenExpiredSessionsExist_AndUnderB context.ServerSideSessions.Add(expiredSession1); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([expiredSession0, expiredSession1]); + IdentityServerServerSideSessionStore sut = CreateSut(context); List actual = (await sut.GetAndRemoveExpiredSessions()).ToList(); - actual.Should().HaveCount(2); - actual.Should().Contain(x => x.Key == expiredSession0.Key); - actual.Should().Contain(x => x.Key == expiredSession1.Key); + actual.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -669,13 +722,13 @@ public async Task GetAndRemoveExpiredSessions_WhenExpiredSessionsExist_AndExceed context.ServerSideSessions.Add(validSession0); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([expiredSession0, expiredSession1]); + IdentityServerServerSideSessionStore sut = CreateSut(context); List actual = (await sut.GetAndRemoveExpiredSessions(2)).ToList(); - actual.Should().HaveCount(2); - actual.Should().Contain(x => x.Key == expiredSession0.Key); - actual.Should().Contain(x => x.Key == expiredSession1.Key); + actual.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -693,13 +746,13 @@ public async Task GetAndRemoveExpiredSessions_WhenExtendedStore_ShouldDeleteAndR context.ServerSideSessions.Add(validSession0); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([expiredSession0, expiredSession1]); + ExtendedIdentityServerServerSideSessionStore sut = CreateSutExtended(context); List actual = (await sut.GetAndRemoveExpiredSessions(2)).ToList(); - actual.Should().HaveCount(2); - actual.Should().Contain(x => x.Key == expiredSession0.Key); - actual.Should().Contain(x => x.Key == expiredSession1.Key); + actual.Should().BeEquivalentTo(expectedExpiredSessions); foreach (var session in actual) { @@ -735,12 +788,13 @@ public async Task GetAndRemoveExpiredSessions_WhenUnspecifiedTimezoneInDbEntitie context.ServerSideSessions.Add(validSession0); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([expiredSession0]); + IdentityServerServerSideSessionStore sut = CreateSut(context); List actual = (await sut.GetAndRemoveExpiredSessions(2)).ToList(); - actual.Should().HaveCount(1); - actual.Should().Contain(x => x.Key == expiredSession0.Key); + actual.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -766,16 +820,20 @@ public async Task FilterSessions_WithQuery_WhenNullQuery_ShouldUseDefaultValues( await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); - await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - ]); + List sessionEntities = [ + new() { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new() { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + new() { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new() { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new() { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new() { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new() { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, + ]; + + await context.ServerSideSessions.AddRangeAsync(sessionEntities, TestContext.Current.CancellationToken); await context.SaveChangesAsync(); + + List expectedExpiredSessions = SetupExpectedSessions(sessionEntities); var actual = await sut.FilterSessions(null, TestContext.Current.CancellationToken); @@ -789,14 +847,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be(expectedToken); actual.HasPrevResults.Should().BeFalse(); actual.HasNextResults.Should().BeFalse(); - actual.Results.Should().HaveCount(7); - actual.Results.Should().Contain(x => x.Key == "key-0"); - actual.Results.Should().Contain(x => x.Key == "key-1"); - actual.Results.Should().Contain(x => x.Key == "key-2"); - actual.Results.Should().Contain(x => x.Key == "key-3"); - actual.Results.Should().Contain(x => x.Key == "key-4"); - actual.Results.Should().Contain(x => x.Key == "key-5"); - actual.Results.Should().Contain(x => x.Key == "key-6"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -804,17 +855,22 @@ public async Task FilterSessions_WithQuery_WhenNoTokenInQuery_ShouldGetFirstPage { await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + + Entities.ServerSideSession foundSessionOne = new () { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }; await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + foundSessionOne, + foundSessionTwo, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" } ]); await context.SaveChangesAsync(); + + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); var actual = await sut.FilterSessions(new SessionQuery { @@ -832,9 +888,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be(expectedToken); actual.HasPrevResults.Should().BeFalse(); actual.HasNextResults.Should().BeTrue(); - actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-0"); - actual.Results.Should().Contain(x => x.Key == "key-1"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -843,17 +897,21 @@ public async Task FilterSessions_WithQuery_WhenTokenInQueryAndGetPreviousFalse_S await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + foundSessionOne, ]); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne]); + var sessions = context.ServerSideSessions .OrderBy(x => x.Id).Skip(4).Take(2).ToList(); var testToken = $"{sessions.First().Id},{sessions.Last().Id}"; @@ -875,8 +933,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be(expectedToken); actual.HasPrevResults.Should().BeTrue(); actual.HasNextResults.Should().BeFalse(); - actual.Results.Should().HaveCount(1); - actual.Results.Should().Contain(x => x.Key == "key-6"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -885,17 +942,22 @@ public async Task FilterSessions_WithQuery_WhenTokenInQueryAndGetPreviousTrue_Sh await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + foundSessionOne, + foundSessionTwo, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); + var sessions = context.ServerSideSessions .OrderBy(x => x.Id).Skip(4).Take(2).ToList(); // Ids 5,6 @@ -914,9 +976,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be("3,4"); actual.HasPrevResults.Should().BeTrue(); actual.HasNextResults.Should().BeTrue(); - actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-2"); - actual.Results.Should().Contain(x => x.Key == "key-3"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -926,13 +986,13 @@ public async Task FilterSessions_WhenOnFinalPage_GetNextPageReturnsEmptyQueryRes IdentityServerServerSideSessionStore sut = CreateSut(context); await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); @@ -958,13 +1018,13 @@ public async Task FilterSessions_WhenOnFirstPage_GetNextPreviousPageReturnsEmpty IdentityServerServerSideSessionStore sut = CreateSut(context); await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); @@ -989,20 +1049,25 @@ public async Task FilterSessions_WhenUnderlyingDataChangedAndResultTokenSpansPag await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, + foundSessionOne, + foundSessionTwo, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); + var sessions = context.ServerSideSessions .OrderBy(x => x.Id).Skip(3).Take(2).ToList(); // 4,5 @@ -1021,9 +1086,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be($"5,6"); actual.HasPrevResults.Should().BeTrue(); actual.HasNextResults.Should().BeTrue(); - actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-4"); - actual.Results.Should().Contain(x => x.Key == "key-5"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -1032,20 +1095,25 @@ public async Task FilterSessions_WhenUnderlyingDataChangedAndResultTokenSpansPag await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, + foundSessionOne, + foundSessionTwo, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); + var sessions = context.ServerSideSessions .OrderBy(x => x.Id).Skip(3).Take(2).ToList(); // 4,5 @@ -1064,9 +1132,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be($"3,4"); actual.HasPrevResults.Should().BeTrue(); actual.HasNextResults.Should().BeTrue(); - actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-2"); - actual.Results.Should().Contain(x => x.Key == "key-3"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -1075,20 +1141,24 @@ public async Task FilterSessions_WhenMovingToFinalPageAndResultsAreLessThanPageS await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + foundSessionOne, ]); await context.SaveChangesAsync(); + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne]); + var sessions = context.ServerSideSessions .OrderBy(x => x.Id).Skip(4).Take(2).ToList(); // 5,6 @@ -1107,8 +1177,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be($"7,7"); actual.HasPrevResults.Should().BeTrue(); actual.HasNextResults.Should().BeFalse(); - actual.Results.Should().HaveCount(1); - actual.Results.Should().Contain(x => x.Key == "key-6"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -1117,16 +1186,21 @@ public async Task FilterSessions_WithQuery_WhenSessionIdProvided_ShouldGetFilter await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + foundSessionOne, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + foundSessionTwo, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); + + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); var actual = await sut.FilterSessions(new SessionQuery { @@ -1147,9 +1221,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be(expectedToken); actual.HasPrevResults.Should().BeFalse(); actual.HasNextResults.Should().BeFalse(); - actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-0"); - actual.Results.Should().Contain(x => x.Key == "key-4"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -1158,16 +1230,21 @@ public async Task FilterSessions_WithQuery_WhenSubjectIdProvided_ShouldGetFilter await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + foundSessionOne, + new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }, + foundSessionTwo, + new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); + + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); var actual = await sut.FilterSessions(new SessionQuery { @@ -1188,9 +1265,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be(expectedToken); actual.HasPrevResults.Should().BeFalse(); actual.HasNextResults.Should().BeTrue(); - actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-0"); - actual.Results.Should().Contain(x => x.Key == "key-2"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -1199,16 +1274,21 @@ public async Task FilterSessions_WithQuery_WhenDisplayNameProvided_ShouldGetFilt await using var context = await CreateCleanContext(options); IdentityServerServerSideSessionStore sut = CreateSut(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-1", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-3", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + foundSessionOne, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + foundSessionTwo, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); + + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); var actual = await sut.FilterSessions(new SessionQuery { @@ -1229,9 +1309,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.ResultsToken.Should().Be(expectedToken); actual.HasPrevResults.Should().BeFalse(); actual.HasNextResults.Should().BeTrue(); - actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-1"); - actual.Results.Should().Contain(x => x.Key == "key-3"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); } [Theory, MemberData(nameof(TestDatabaseProviders))] @@ -1240,16 +1318,21 @@ public async Task FilterSessions_WithExtendedStore_ShouldGetFilteredResultContai await using var context = await CreateCleanContext(options); ExtendedIdentityServerServerSideSessionStore sut = CreateSutExtended(context); + Entities.ServerSideSession foundSessionOne = new () { Key = "key-1", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-1\"}" }; + Entities.ServerSideSession foundSessionTwo = new () { Key = "key-3", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-3", Data = "{\"payload\":\"fake-payload-key-3\"}" }; + await context.ServerSideSessions.AddRangeAsync([ - new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-1", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-3", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-3", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"delete\":true}" }, - new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"delete\":true}" }, + new Entities.ServerSideSession { Key = "key-0", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-0\"}" }, + foundSessionOne, + new Entities.ServerSideSession { Key = "key-2", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-2\"}" }, + foundSessionTwo, + new Entities.ServerSideSession { Key = "key-4", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-0", Data = "{\"payload\":\"fake-payload-key-4\"}" }, + new Entities.ServerSideSession { Key = "key-5", Scheme = "cookie", DisplayName = "Robert", SubjectId = "bob", SessionId = "session-2", Data = "{\"payload\":\"fake-payload-key-5\"}" }, + new Entities.ServerSideSession { Key = "key-6", Scheme = "cookie", DisplayName = "Laura", SubjectId = "alice", SessionId = "session-1", Data = "{\"payload\":\"fake-payload-key-6\"}" }, ]); await context.SaveChangesAsync(); + + List expectedExpiredSessions = SetupExpectedSessions([foundSessionOne, foundSessionTwo]); var actual = await sut.FilterSessions(new SessionQuery { @@ -1271,8 +1354,7 @@ await context.ServerSideSessions.AddRangeAsync([ actual.HasPrevResults.Should().BeFalse(); actual.HasNextResults.Should().BeTrue(); actual.Results.Should().HaveCount(2); - actual.Results.Should().Contain(x => x.Key == "key-1"); - actual.Results.Should().Contain(x => x.Key == "key-3"); + actual.Results.Should().BeEquivalentTo(expectedExpiredSessions); foreach (var session in actual.Results) { @@ -1336,27 +1418,37 @@ private async Task CreateCleanContext(DbContextOptions< return context; } - private static ServerSideSession BuildSessionModel(string key, string subjectId, string sessionId, string displayName) + private static ServerSideSession BuildSessionModel(string key, string subjectId, string sessionId, + string displayName, string? scheme = null, DateTime? created = null, DateTime? renewed = null, DateTime? expires = null, + string? data = null) { + scheme ??= "cookie"; + created ??= FakeNow.AddMinutes(-10); + renewed ??= FakeNow.AddMinutes(-5); + // expires ??= FakeNow.AddHours(1); + return new ServerSideSession { Key = key, - Scheme = "cookie", + Scheme = scheme, SubjectId = subjectId, SessionId = sessionId, DisplayName = displayName, - Created = FakeNow.AddMinutes(-10), - Renewed = FakeNow.AddMinutes(-5), - Expires = FakeNow.AddHours(1), - Data = "{\"payload\":\"value\"}" + Created = created.Value, + Renewed = renewed.Value, + Expires = expires, + Data = data!, + AuthenticationTicket = AuthenticationTicketGenerator.GenerateAuthenticationTicket(scheme, subjectId, sessionId, displayName, renewed, expires) }; } private static Entities.ServerSideSession FakeSessionSession(string subject, string sessionId, bool expired = false) { + string key = Guid.NewGuid().ToString(); + Entities.ServerSideSession session = new Entities.ServerSideSession { - Key = Guid.NewGuid().ToString(), + Key = key, Scheme = Guid.NewGuid().ToString(), SubjectId = subject, SessionId = sessionId, @@ -1364,7 +1456,7 @@ private static Entities.ServerSideSession FakeSessionSession(string subject, str Created = FakeNow.AddDays(-3), Renewed = FakeNow.AddDays(-3), Expires = FakeNow.AddDays(2), - Data = "{!}" + Data = $"{{\"payload\":\"fake-payload-{key}\"}}" }; if (expired) diff --git a/src/EntityFramework.Storage/test/UnitTests/Mappers/IdentityServerServerSideSessionsExtensionsTests.cs b/src/EntityFramework.Storage/test/UnitTests/Mappers/IdentityServerServerSideSessionsExtensionsTests.cs index 35ba381e6..df74e91e1 100644 --- a/src/EntityFramework.Storage/test/UnitTests/Mappers/IdentityServerServerSideSessionsExtensionsTests.cs +++ b/src/EntityFramework.Storage/test/UnitTests/Mappers/IdentityServerServerSideSessionsExtensionsTests.cs @@ -3,14 +3,20 @@ using System; using AwesomeAssertions; +using Microsoft.AspNetCore.Authentication; +using Moq; using Open.IdentityServer.EntityFramework.Entities; using Open.IdentityServer.EntityFramework.Mappers; +using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.Test.Utilities; using Xunit; namespace Open.IdentityServer.EntityFramework.UnitTests.Mappers; public class IdentityServerServerSideSessionsExtensionsTests { + private readonly IAuthenticationTicketSerializer authTicketSerializer = Mock.Of(); + [Fact] public void EntityIdentityServerServerSideSessions_ToModel_ProducesModelWithCorrectFields() { @@ -25,12 +31,17 @@ public void EntityIdentityServerServerSideSessions_ToModel_ProducesModelWithCorr Created = new DateTime(2020, 01, 01, 12, 20, 0, DateTimeKind.Utc), Renewed = new DateTime(2020, 02, 01, 12, 20, 0, DateTimeKind.Utc), Expires = new DateTime(2020, 03, 01, 12, 20, 0, DateTimeKind.Utc), - Data = "FAKEPROTECTEDDATA" + Data = "{\"version\":1,\"payload\":\"fake-payload-FakeScheme\"}", }; + AuthenticationTicket authTicket = AuthenticationTicketGenerator.GenerateAuthenticationTicket(entity.Scheme, entity.SubjectId, entity.SessionId, entity.DisplayName, entity.Renewed, entity.Expires); - Models.ServerSideSession actual = entity.ToModel(); + Mock.Get(authTicketSerializer) + .Setup(x => x.Deserialize(entity.Data)) + .Returns(authTicket); + + Models.ServerSideSession actual = entity.ToModel(authTicketSerializer); - entity.Should().BeEquivalentTo(actual); + actual.Should().BeEquivalentTo(entity, cnf => cnf.Excluding(x => x.Id)); } [Fact] @@ -46,11 +57,20 @@ public void ModelIdentityServerServerSideSessions_ToEntity_ProducesEntityWithCor Created = new DateTime(2020, 01, 01, 12, 20, 0, DateTimeKind.Utc), Renewed = new DateTime(2020, 02, 01, 12, 20, 0, DateTimeKind.Utc), Expires = new DateTime(2020, 03, 01, 12, 20, 0, DateTimeKind.Utc), - Data = "FAKEPROTECTEDDATA" }; + AuthenticationTicket authTicket = AuthenticationTicketGenerator.GenerateAuthenticationTicket(model.Scheme, model.SubjectId, model.SessionId, model.DisplayName, model.Renewed, model.Expires); + model.AuthenticationTicket = authTicket; + + const string fakePayload = "{\"version\":1,\"payload\":\"fake-payload-FakeScheme\"}"; + Mock.Get(authTicketSerializer) + .Setup(x => x.Serialize(model.AuthenticationTicket)) + .Returns(fakePayload); - ServerSideSession actual = model.ToEntity(); + ServerSideSession actual = model.ToEntity(authTicketSerializer); - model.Should().BeEquivalentTo(actual, cnf => cnf.Excluding(x => x.Id)); + actual.Should().BeEquivalentTo(model, cnf => cnf + .Excluding(x => x.AuthenticationTicket) + .Excluding(x => x.Data)); + actual.Data.Should().BeEquivalentTo(fakePayload); } } \ No newline at end of file diff --git a/src/EntityFramework.Storage/test/UnitTests/Open.IdentityServer.EntityFramework.UnitTests.csproj b/src/EntityFramework.Storage/test/UnitTests/Open.IdentityServer.EntityFramework.UnitTests.csproj index 8d15a86af..7338e0e14 100644 --- a/src/EntityFramework.Storage/test/UnitTests/Open.IdentityServer.EntityFramework.UnitTests.csproj +++ b/src/EntityFramework.Storage/test/UnitTests/Open.IdentityServer.EntityFramework.UnitTests.csproj @@ -10,11 +10,13 @@ + + diff --git a/src/Open.IdentityServer.Test.Utilities/AuthenticationTicketGenerator.cs b/src/Open.IdentityServer.Test.Utilities/AuthenticationTicketGenerator.cs new file mode 100644 index 000000000..9667fe0f1 --- /dev/null +++ b/src/Open.IdentityServer.Test.Utilities/AuthenticationTicketGenerator.cs @@ -0,0 +1,122 @@ +using System.Buffers.Text; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; + +// using Microsoft.AspNetCore.Authentication; + +namespace Open.IdentityServer.Test.Utilities; + +public static class TestsClaimTypeConstants +{ + public const string Subject = "sub"; + public const string Name = "name"; + public const string IdentityProvider = "idp"; + public const string AuthenticationTime = "auth_time"; + public const string AuthenticationMethod = "amr"; + public const string Role = "role"; + public const string Issuer = "iss"; + public const string SessionId = "session_id"; + public const string ClientListKey = "client_list"; +} + +public static class AuthenticationTicketGenerator +{ + public static ClaimsPrincipal CreatePrincipal( + string subjectId, + string? displayName = null, + string? identityProvider = null, + DateTime? authenticationTime = null, + string[]? authenticationMethods = null, + Claim[]? additionalClaims = null) + { + if (string.IsNullOrWhiteSpace(subjectId)) throw new ArgumentException("SubjectId is mandatory", nameof(subjectId)); + var claims = new List { new Claim(TestsClaimTypeConstants.Subject, subjectId) }; + + if (!string.IsNullOrWhiteSpace(displayName)) + { + claims.Add(new Claim(TestsClaimTypeConstants.Name, displayName)); + } + + if (!string.IsNullOrWhiteSpace(identityProvider)) + { + claims.Add(new Claim(TestsClaimTypeConstants.IdentityProvider, identityProvider)); + } + + if (authenticationTime.HasValue) + { + claims.Add(new Claim(TestsClaimTypeConstants.AuthenticationTime, new DateTimeOffset(authenticationTime.Value).ToUnixTimeSeconds().ToString())); + } + + if (authenticationMethods != null && authenticationMethods.Length != 0) + { + foreach (var amr in authenticationMethods) + { + claims.Add(new Claim(TestsClaimTypeConstants.AuthenticationMethod, amr)); + } + } + + if (additionalClaims != null) + { + claims.AddRange(additionalClaims); + } + + var id = new ClaimsIdentity(claims, "Open.IdentityServer", TestsClaimTypeConstants.Name, TestsClaimTypeConstants.Role); + return new ClaimsPrincipal(id); + } + + public static AuthenticationTicket GenerateAuthenticationTicket( + string authScheme, + string? subjectId, + string? sessionId, + string? displayName = null, + DateTimeOffset? issuedUtc = null, + DateTimeOffset? expiresUtc = null, + string? issuer = null, + string[]? clientIds = null) + { + ClaimsPrincipal principle = CreatePrincipal(subjectId ?? string.Empty, displayName: displayName); + AuthenticationProperties properties = new(); + + properties.Items[TestsClaimTypeConstants.SessionId] = sessionId; + properties.IssuedUtc = issuedUtc; + properties.ExpiresUtc = expiresUtc; + + if (clientIds != null && clientIds.Any()) + { + properties.Items[TestsClaimTypeConstants.ClientListKey] = GeneratePropertyList(clientIds); + } + + if (issuer != null) + { + properties.Items[TestsClaimTypeConstants.Issuer] = issuer; + } + + return new AuthenticationTicket(principle, properties, authScheme); + } + + private static string GeneratePropertyList(IEnumerable list) + { + if (list != null && list.Any()) + { + var value = JsonSerializer.Serialize(list); + var bytes = Encoding.UTF8.GetBytes(value); + value = Encode(bytes); + return value; + } + + return null; + } + + private static string Encode(byte[] arg) + { + var s = Convert.ToBase64String(arg); // Standard base64 encoder + + s = s.Split('=')[0]; // Remove any trailing '='s + s = s.Replace('+', '-'); // 62nd char of encoding + s = s.Replace('/', '_'); // 63rd char of encoding + + return s; + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer.Test.Utilities/Open.IdentityServer.Test.Utilities.csproj b/src/Open.IdentityServer.Test.Utilities/Open.IdentityServer.Test.Utilities.csproj index d4095961e..6212256a8 100644 --- a/src/Open.IdentityServer.Test.Utilities/Open.IdentityServer.Test.Utilities.csproj +++ b/src/Open.IdentityServer.Test.Utilities/Open.IdentityServer.Test.Utilities.csproj @@ -7,8 +7,9 @@ - - + + + diff --git a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs index 6b4e0e41f..dd6c7c1f5 100644 --- a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs +++ b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs @@ -16,8 +16,10 @@ using Open.IdentityServer.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; using Open.IdentityServer.Services.Default; +using Open.IdentityServer.Stores.Serialization; namespace Microsoft.Extensions.DependencyInjection; @@ -474,6 +476,7 @@ public static IIdentityServerBuilder AddUserSession(this IIdentityServerBuild public static IIdentityServerBuilder AddServerSideSessions(this IIdentityServerBuilder builder) { builder.Services.AddSingleton, PostConfigureSessionStoreCookieAuthOptions>(); + builder.Services.AddSingleton(); builder.Services.TryAddTransient(); // Token Validators diff --git a/src/Open.IdentityServer/src/DataProtection/AuthenticationTicketSerializer.cs b/src/Open.IdentityServer/src/DataProtection/AuthenticationTicketSerializer.cs new file mode 100644 index 000000000..ae71b71db --- /dev/null +++ b/src/Open.IdentityServer/src/DataProtection/AuthenticationTicketSerializer.cs @@ -0,0 +1,69 @@ +#nullable enable + +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Open.IdentityServer.Extensions; +using Open.IdentityServer.Stores.Serialization; + +namespace Open.IdentityServer.DataProtection; + +/// +/// Serializer class for , data is serialized/deserialized and protected/unprotected using data protection +/// +public class AuthenticationTicketSerializer( + IDataProtectionProvider dataProtectionProvider, + ILogger logger): IAuthenticationTicketSerializer +{ + private readonly IDataProtector dataProtector = + dataProtectionProvider.CreateProtector(DataProtectionConstants.ServerSideTicketStorePurpose); + + /// + /// to be used for storing server side sessions + /// + public static readonly JsonSerializerOptions JsonSettings = new() + { + IncludeFields = true, + }; + + /// + public string Serialize(AuthenticationTicket ticket) + { + string serializedTicket = JsonSerializer.Serialize(ticket.ToSerializableObj()); + + return JsonSerializer.Serialize(new DataProtectedSessionData + { + Payload = dataProtector.Protect(serializedTicket), + }, JsonSettings); + } + + /// + public AuthenticationTicket? Deserialize(string data) + { + DataProtectedSessionData? dataProtectedSessionData; + + try + { + dataProtectedSessionData = JsonSerializer.Deserialize(data, JsonSettings); + } + catch (JsonException exception) + { + logger.LogError(exception, "failed deserialising auth ticket data"); + return null; + } + + if (dataProtectedSessionData is not { Version: 1 }) + { + logger.LogError("failed deserialising auth ticket data, incorrect version '{VersionOrNull}'", dataProtectedSessionData?.Version); + return null; + } + + string unprotectedData = dataProtector.Unprotect(dataProtectedSessionData.Payload); + + SerializedAuthenticationTicket? serializedAuthTicket = + JsonSerializer.Deserialize(unprotectedData); + + return serializedAuthTicket?.ToAuthTicket(); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Extensions/AuthenticationTicketSerialiser.cs b/src/Open.IdentityServer/src/Extensions/AuthenticationTicketSerialiser.cs index ed175cf10..45b1f8d6d 100644 --- a/src/Open.IdentityServer/src/Extensions/AuthenticationTicketSerialiser.cs +++ b/src/Open.IdentityServer/src/Extensions/AuthenticationTicketSerialiser.cs @@ -26,12 +26,19 @@ public SerializedAuthenticationTicket ToSerializableObj() extension(SerializedAuthenticationTicket serializationAuthTicket) { - public AuthenticationTicket ToAuthTicket(ServerSideSession? session) + public AuthenticationTicket ToAuthTicket() { var authTicket = new AuthenticationTicket( serializationAuthTicket.User.ToClaimsPrincipal(), new AuthenticationProperties(serializationAuthTicket.Items), serializationAuthTicket.Scheme); + + return authTicket; + } + + public AuthenticationTicket ToAuthTicket(ServerSideSession session) + { + var authTicket = serializationAuthTicket.ToAuthTicket(); if (session != null) { diff --git a/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs b/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs index 8315bb3ce..22a8a6334 100644 --- a/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs +++ b/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs @@ -99,12 +99,11 @@ internal static void SetSignOutCalled(this HttpContext context) internal static void SetSessionExpired( this HttpContext context, - ServerSideSession session, - AuthenticationTicket ticket) + ServerSideSession session) { if (context == null) throw new ArgumentNullException(nameof(context)); context.Items[Constants.EnvironmentKeys.ExpiredSession] = session; - context.Items[Constants.EnvironmentKeys.ExpiredTicket] = ticket; + context.Items[Constants.EnvironmentKeys.ExpiredTicket] = session.AuthenticationTicket; } internal static (ServerSideSession Session, AuthenticationTicket Ticket) GetSessionExpired(this HttpContext context) diff --git a/src/Open.IdentityServer/src/Models/AuthenticationTicketFilterResult.cs b/src/Open.IdentityServer/src/Models/AuthenticationTicketFilterResult.cs index fcad63492..02b6b8bf4 100644 --- a/src/Open.IdentityServer/src/Models/AuthenticationTicketFilterResult.cs +++ b/src/Open.IdentityServer/src/Models/AuthenticationTicketFilterResult.cs @@ -19,9 +19,9 @@ public class AuthenticationTicketFilterResult /// Session model returned from filtering /// public ServerSideSession Session { get; set; } = null!; - + /// /// AuthenticationTicket deserialized from the data property on the session entity /// - public AuthenticationTicket? AuthTicket { get; set; } + public AuthenticationTicket? AuthTicket => Session.AuthenticationTicket; } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultSessionManagementService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultSessionManagementService.cs index c47c2f1b4..e1cb4a56f 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultSessionManagementService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultSessionManagementService.cs @@ -49,7 +49,7 @@ public async Task RemoveSessionsAsync(RemoveSessionsContext context, Cancellatio var sessions = await serverSessionTicketStore.FilterServerAuthenticationTickets(context.SubjectId, context.SessionId); foreach (var session in sessions) { - List? sessionClientList = session.AuthTicket?.Properties.GetClientList().ToList(); + List? sessionClientList = session.Session.AuthenticationTicket?.Properties.GetClientList().ToList(); string[] clientIds = []; if (!sessionClientList.IsNullOrEmpty() && !context.ClientIds.IsNullOrEmpty()) diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultUserSessionEventsService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultUserSessionEventsService.cs index d5fe669e5..a7a5bd2bb 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultUserSessionEventsService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultUserSessionEventsService.cs @@ -122,14 +122,14 @@ public async Task ValidateSession(ValidateUserSessionEventContext sessionE TimeSpan? diff = session.Session.Expires - session.Session.Renewed; session.Session.Renewed = timeProvider.GetUtcNow().UtcDateTime; session.Session.Expires = session.Session.Renewed + diff; + session.Session.AuthenticationTicket?.Properties.IssuedUtc = session.Session.Renewed; + session.Session.AuthenticationTicket?.Properties.ExpiresUtc = session.Session.Expires; if (idsOptions.Authentication.CookieSlidingExpiration && - session.AuthTicket?.Properties is { IsPersistent: true, AllowRefresh: true or null }) + session.Session.AuthenticationTicket?.Properties is { IsPersistent: true, AllowRefresh: true or null }) { - session.AuthTicket.Properties.IssuedUtc = session.Session.Renewed; - session.AuthTicket.Properties.ExpiresUtc = session.Session.Expires; - session.AuthTicket.Properties.SetString(IdentityServerConstants.ForceCookieRefresh, string.Empty); - await serverSessionTicketStore.RenewAsync(session.Session.Key, session.AuthTicket); + session.Session.AuthenticationTicket.Properties.SetString(IdentityServerConstants.ForceCookieRefresh, string.Empty); + await serverSessionTicketStore.RenewAsync(session.Session.Key, session.Session.AuthenticationTicket); } else { diff --git a/src/Open.IdentityServer/src/Services/Default/SessionCleanupService.cs b/src/Open.IdentityServer/src/Services/Default/SessionCleanupService.cs index ad655cbaa..f3925e0d6 100644 --- a/src/Open.IdentityServer/src/Services/Default/SessionCleanupService.cs +++ b/src/Open.IdentityServer/src/Services/Default/SessionCleanupService.cs @@ -70,7 +70,7 @@ await userSessionEventsService.HandleUserSessionExpiry(new EndUserSessionEventCo { SubjectId = expiredSession.Session.SubjectId, SessionId = expiredSession.Session.SessionId, - ClientIds = expiredSession.AuthTicket?.Properties.GetClientList().ToArray() ?? [], + ClientIds = expiredSession.Session.AuthenticationTicket?.Properties.GetClientList().ToArray() ?? [], }); } } while (found >= options.ServerSideSessions.RemoveExpiredSessionsBatchSize); diff --git a/src/Open.IdentityServer/src/Stores/Default/ServerSessionTicketStore.cs b/src/Open.IdentityServer/src/Stores/Default/ServerSessionTicketStore.cs index 560dc810e..86f52f3ae 100644 --- a/src/Open.IdentityServer/src/Stores/Default/ServerSessionTicketStore.cs +++ b/src/Open.IdentityServer/src/Stores/Default/ServerSessionTicketStore.cs @@ -7,21 +7,17 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Open.IdentityServer.Configuration; -using Open.IdentityServer.DataProtection; using Open.IdentityServer.Extensions; using Open.IdentityServer.Models; using Open.IdentityServer.Services; using Open.IdentityServer.Stores.Default; -using Open.IdentityServer.Stores.Serialization; namespace Open.IdentityServer.Stores; @@ -30,7 +26,6 @@ namespace Open.IdentityServer.Stores; /// implementation in Open.IdentityServer /// /// -/// data protection provider /// persisted grant store /// handle generation service /// time provider @@ -40,7 +35,6 @@ namespace Open.IdentityServer.Stores; /// the logger public class ServerSessionTicketStore( IIdentityServerServerSideSessionStore serverServerSideSessionStore, - IDataProtectionProvider dataProtectionProvider, IPersistedGrantStore persistedGrantStore, IHandleGenerationService handleGenerationService, TimeProvider timeProvider, @@ -49,17 +43,6 @@ public class ServerSessionTicketStore( ITelemetryService telemetry, ILogger logger): IServerSessionTicketStore { - private readonly IDataProtector dataProtector = - dataProtectionProvider.CreateProtector(DataProtectionConstants.ServerSideTicketStorePurpose); - - /// - /// to be used for storing server side sessions - /// - public static readonly JsonSerializerOptions JsonSettings = new() - { - IncludeFields = true, - }; - /// public async Task StoreAsync(AuthenticationTicket ticket) { @@ -115,7 +98,7 @@ await persistedGrantStore.RemoveAllAsync(new PersistedGrantFilter existingSession.DisplayName = ticket.Principal.FindFirstValue(JwtClaimTypes.Name); existingSession.Renewed = ticket.Properties.IssuedUtc?.UtcDateTime ?? timeProvider.GetUtcNow().UtcDateTime; existingSession.Expires = ticket.Properties.ExpiresUtc?.UtcDateTime; - existingSession.Data = ToProtectedDataString(ticket); + existingSession.AuthenticationTicket = ticket; await serverServerSideSessionStore.UpdateSession(existingSession); } @@ -138,17 +121,16 @@ await persistedGrantStore.RemoveAllAsync(new PersistedGrantFilter return null; } - var authTicket = DeserializeAuthTicket(existingSession); - if(authTicket == null) + if(existingSession.AuthenticationTicket == null) { logger.FailedToRetrieveTicketFromStore(key); } else { - logger.TicketFoundInStore(key, authTicket.Properties.ExpiresUtc); + logger.TicketFoundInStore(key, existingSession.AuthenticationTicket.Properties.ExpiresUtc); } - return authTicket; + return existingSession.AuthenticationTicket; } /// @@ -165,7 +147,7 @@ public async Task RemoveAsync(string key) var session = await serverServerSideSessionStore.GetSession(key); if(session != null) { - httpContextAccessor.HttpContext.SetSessionExpired(session, DeserializeAuthTicket(session)); + httpContextAccessor.HttpContext.SetSessionExpired(session); } await serverServerSideSessionStore.DeleteSession(key); @@ -176,13 +158,8 @@ public async Task> FilterServerAut { using ITrace? trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); - IEnumerable sessions = await serverServerSideSessionStore.FilterSessions(subjectId, sessionId); - - return sessions.Select(x => new AuthenticationTicketFilterResult - { - Session = x, - AuthTicket = DeserializeAuthTicket(x), - }).Where(x => x.AuthTicket != null); + return (await serverServerSideSessionStore.FilterSessions(subjectId, sessionId)) + .Select(x => new AuthenticationTicketFilterResult { Session = x }); } /// @@ -190,13 +167,8 @@ public async Task> FilterServerAut { using ITrace? trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); - QueryResult sessions = await serverServerSideSessionStore.FilterSessions(query, ct); - - return sessions.MapTo(x => new AuthenticationTicketFilterResult - { - Session = x, - AuthTicket = DeserializeAuthTicket(x), - }); + return (await serverServerSideSessionStore.FilterSessions(query, ct)) + .MapTo(x => new AuthenticationTicketFilterResult { Session = x }); } /// @@ -204,13 +176,8 @@ public async Task> GetAndRemoveExp { using ITrace? trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); - var sessionRemoved = await serverServerSideSessionStore.GetAndRemoveExpiredSessions(batchSize); - - return sessionRemoved.Select(x => new AuthenticationTicketFilterResult - { - Session = x, - AuthTicket = DeserializeAuthTicket(x), - }); + return (await serverServerSideSessionStore.GetAndRemoveExpiredSessions(batchSize)) + .Select(x => new AuthenticationTicketFilterResult { Session = x }); } private async Task StoreNewSession(string key, AuthenticationTicket ticket) @@ -238,49 +205,27 @@ private async Task StoreNewSession(string key, Authentication Created = ticket.Properties.IssuedUtc?.UtcDateTime ?? timeProvider.GetUtcNow().UtcDateTime, Renewed = ticket.Properties.IssuedUtc?.UtcDateTime ?? timeProvider.GetUtcNow().UtcDateTime, Expires = ticket.Properties.ExpiresUtc?.UtcDateTime, - Data = ToProtectedDataString(ticket), + AuthenticationTicket = ticket, }; await serverServerSideSessionStore.CreateSession(serverSideSession); return serverSideSession; } - - private string ToProtectedDataString(AuthenticationTicket ticket) - { - string serializedTicket = JsonSerializer.Serialize(ticket.ToSerializableObj()); - - return JsonSerializer.Serialize(new DataProtectedSessionData - { - Payload = dataProtector.Protect(serializedTicket), - }, JsonSettings); - } - private AuthenticationTicket? DeserializeAuthTicket(ServerSideSession existingSession) - { - DataProtectedSessionData? dataProtectedSessionData; - - try - { - dataProtectedSessionData = JsonSerializer.Deserialize(existingSession.Data, JsonSettings); - } - catch (JsonException exception) - { - logger.LogError(exception, "failed deserialising auth ticket data '{SessionKey}'", existingSession.Key); - return null; - } - - if (dataProtectedSessionData is not { Version: 1 }) - { - logger.LogError("failed retrieving '{SessionKey}', deserialisation failed, incorrect version '{VersionOrNull}'", existingSession.Key, dataProtectedSessionData?.Version); - return null; - } - - string unprotectedData = dataProtector.Unprotect(dataProtectedSessionData.Payload); - - SerializedAuthenticationTicket? serializedAuthTicket = - JsonSerializer.Deserialize(unprotectedData); - - return serializedAuthTicket?.ToAuthTicket(existingSession); - } + // private AuthenticationTicket? DeserializeAuthTicket(ServerSideSession existingSession) + // { + // AuthenticationTicket? authenticationTicket = authenticationTicketSerializer.Deserialize(existingSession.Data); + // + // if (authenticationTicket != null) + // { + // authenticationTicket.Properties.IssuedUtc = new DateTimeOffset(existingSession.Renewed, TimeSpan.Zero); + // if (existingSession.Expires.HasValue) + // { + // authenticationTicket.Properties.ExpiresUtc = new DateTimeOffset(existingSession.Expires.Value, TimeSpan.Zero); + // } + // } + // + // return authenticationTicket; + // } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Stores/IServerSessionTicketStore.cs b/src/Open.IdentityServer/src/Stores/IServerSessionTicketStore.cs index ff9562820..410b98b75 100644 --- a/src/Open.IdentityServer/src/Stores/IServerSessionTicketStore.cs +++ b/src/Open.IdentityServer/src/Stores/IServerSessionTicketStore.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Open.IdentityServer.Models; diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Configuration/DependencyInjection/BuilderExtensions/AdditionalTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Configuration/DependencyInjection/BuilderExtensions/AdditionalTests.cs index d51579fd4..0f3a4164a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Configuration/DependencyInjection/BuilderExtensions/AdditionalTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Configuration/DependencyInjection/BuilderExtensions/AdditionalTests.cs @@ -11,10 +11,11 @@ using Microsoft.Extensions.Options; using Open.IdentityServer.Configuration; using Open.IdentityServer.Configuration.DependencyInjection; +using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; using Open.IdentityServer.Services; -using Open.IdentityServer.Services.Default; using Open.IdentityServer.Stores; +using Open.IdentityServer.Stores.Serialization; using Open.IdentityServer.Validation; using Xunit; @@ -43,6 +44,11 @@ public void AddServerSideSessions_WhenNoStoreConfigured_ShouldConfigureServerSid d.ImplementationType == typeof(PostConfigureSessionStoreCookieAuthOptions) && d.Lifetime == ServiceLifetime.Singleton); + serviceCollection.Should().ContainSingle(d => + d.ServiceType == typeof(IAuthenticationTicketSerializer) && + d.ImplementationType == typeof(AuthenticationTicketSerializer) && + d.Lifetime == ServiceLifetime.Singleton); + serviceCollection.Should().ContainSingle(d => d.ServiceType == typeof(IServerSessionTicketStore) && d.ImplementationType == typeof(ServerSessionTicketStore) && @@ -127,6 +133,11 @@ public void AddServerSideSessions_WhenStoreConfigured_ShouldConfigureServerSideS d.ImplementationType == typeof(PostConfigureSessionStoreCookieAuthOptions) && d.Lifetime == ServiceLifetime.Singleton); + serviceCollection.Should().ContainSingle(d => + d.ServiceType == typeof(IAuthenticationTicketSerializer) && + d.ImplementationType == typeof(AuthenticationTicketSerializer) && + d.Lifetime == ServiceLifetime.Singleton); + serviceCollection.Should().ContainSingle(d => d.ServiceType == typeof(IServerSessionTicketStore) && d.ImplementationType == typeof(ServerSessionTicketStore) && diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/AuthenticationTicketSerializerTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/AuthenticationTicketSerializerTests.cs new file mode 100644 index 000000000..c52fe14b4 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/AuthenticationTicketSerializerTests.cs @@ -0,0 +1,118 @@ +#nullable enable + +using System; +using System.Text.Json; +using System.Threading.Tasks; +using AwesomeAssertions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Moq; +using Open.IdentityServer.DataProtection; +using Open.IdentityServer.Extensions; +using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.Test.Utilities; +using Open.IdentityServer.UnitTests.Utilities.Generators; +using Xunit; + +namespace Open.IdentityServer.UnitTests.DataProtection; + +public class AuthenticationTicketSerializerTests +{ + private readonly IDataProtectionProvider dataProtectionProvider = Mock.Of(); + private readonly MockDataProtector dataProtector = new(); + private readonly MockLogger logger = new(); + + public AuthenticationTicketSerializerTests() + { + Mock.Get(dataProtectionProvider) + .Setup(x => x.CreateProtector(DataProtectionConstants.ServerSideTicketStorePurpose)) + .Returns(dataProtector); + } + + private AuthenticationTicketSerializer CreateSut() => new(dataProtectionProvider, logger); + + [Fact] + public async Task Serialize_When_Should() + { + const string authScheme = "FakeAuthScheme"; + string subjectId = Guid.NewGuid().ToString(); + string sessionId = Guid.NewGuid().ToString(); + const string displayName = "Fake User"; + DateTime issuedUtc = new(2026, 02, 19, 12, 0, 0, DateTimeKind.Utc); + DateTime expiresUtc = new(2026, 02, 19, 12, 0, 0, DateTimeKind.Utc); + string fakeIssuer = "https://fake.issuer.com"; + + AuthenticationTicket authenticationTicket = + ServerSessionTestGenerators.GenerateAuthenticationTicket(authScheme, subjectId, sessionId, displayName, issuedUtc, expiresUtc, issuer: fakeIssuer); + + AuthenticationTicketSerializer sut = CreateSut(); + + var actualData = sut.Serialize(authenticationTicket); + + var jsonElement = JsonElement.Parse(actualData); + + jsonElement.GetProperty("Version").GetInt32().Should().Be(1); + var actualPayload = jsonElement.GetProperty("Payload").GetString(); + actualPayload.Should().NotBeNull(); + + var expectedAuthTicket = authenticationTicket.Clone(); + expectedAuthTicket.Properties.Items[JwtClaimTypes.Issuer] = fakeIssuer; + + string expectedJson = JsonSerializer.Serialize(expectedAuthTicket.ToSerializableObj(), + AuthenticationTicketSerializer.JsonSettings); + dataProtector.ValidateProtectedData(actualPayload, expectedJson); + } + + [Fact] + public async Task Deserialize_When_Should() + { + const string authScheme = "FakeAuthScheme"; + string subjectId = Guid.NewGuid().ToString(); + string sessionId = Guid.NewGuid().ToString(); + const string displayName = "Fake User"; + DateTime issuedUtc = new(2026, 02, 19, 12, 0, 0, DateTimeKind.Utc); + DateTime expiresUtc = new(2026, 02, 19, 12, 0, 0, DateTimeKind.Utc); + string fakeIssuer = "https://fake.issuer.com"; + + SerializedAuthenticationTicket authenticationTicket = ServerSessionTestGenerators.GenerateSerializedAuthenticationTicket(authScheme, subjectId, sessionId, displayName, issuedUtc, expiresUtc, issuer: fakeIssuer); + + AuthenticationTicketSerializer sut = CreateSut(); + + string fakeProtectedData = GenerateFakeProtectedData(authenticationTicket); + AuthenticationTicket? actual = sut.Deserialize(fakeProtectedData); + + actual.Should().BeOfType(); + actual.AuthenticationScheme.Should().Be(authScheme); + actual.Principal.Identity.Should().NotBeNull(); + actual.Principal.Identity?.AuthenticationType.Should() + .BeEquivalentTo(authenticationTicket.User.AuthenticationType); + actual.Principal.Identity?.Name.Should().BeEquivalentTo(displayName); + actual.Principal.Identities.Should().Contain(x => + x.NameClaimType == JwtClaimTypes.Name && + x.RoleClaimType == JwtClaimTypes.Role); + actual.Properties.Items.Should().BeEquivalentTo(authenticationTicket.Items); + } + + [Theory] + [InlineData("{invalid.json}")] + [InlineData("{\"Version\": 2, Payload: \"SOMEDATA\"}")] + public async Task Deserialize_WhenSessionDataDeserialisationFails_ShouldReturnNull(string data) + { + AuthenticationTicketSerializer sut = CreateSut(); + + AuthenticationTicket? actual = sut.Deserialize(data); + + actual.Should().BeNull(); + } + + private string GenerateFakeProtectedData(SerializedAuthenticationTicket serializedAuthenticationTicket) + { + DataProtectedSessionData sessionData = new DataProtectedSessionData + { + Payload = dataProtector.GenerateFakeProtectedData(JsonSerializer.Serialize(serializedAuthenticationTicket, + AuthenticationTicketSerializer.JsonSettings)) + }; + + return JsonSerializer.Serialize(sessionData, AuthenticationTicketSerializer.JsonSettings); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/Mapping/AuthenticationTicketFilterResultMappingExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/Mapping/AuthenticationTicketFilterResultMappingExtensionsTests.cs index b1e0818ad..22f5b685d 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/Mapping/AuthenticationTicketFilterResultMappingExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/Mapping/AuthenticationTicketFilterResultMappingExtensionsTests.cs @@ -22,7 +22,7 @@ public void ToUserSession_ShouldMapAuthTicketResultToUserSession() actualSession.Created.Should().Be(sut.Session.Created); actualSession.Renewed.Should().Be(sut.Session.Renewed); actualSession.Expires.Should().Be(sut.Session.Expires); - actualSession.AuthenticationTicket.Should().BeEquivalentTo(sut.AuthTicket); + actualSession.AuthenticationTicket.Should().BeEquivalentTo(sut.Session.AuthenticationTicket); actualSession.Issuer.Should().BeNullOrWhiteSpace(); actualSession.ClientIds.Should().BeNullOrEmpty(); } @@ -42,8 +42,8 @@ public void ToUserSession_WhenClientIdsSet_ShouldExtractClientIdsInResult() actualSession.Renewed.Should().Be(sut.Session.Renewed); actualSession.Expires.Should().Be(sut.Session.Expires); actualSession.Issuer.Should().BeNullOrWhiteSpace(); - actualSession.AuthenticationTicket.Should().BeEquivalentTo(sut.AuthTicket); - actualSession.ClientIds.Should().BeEquivalentTo(sut.AuthTicket!.Properties.GetClientList()); + actualSession.AuthenticationTicket.Should().BeEquivalentTo(sut.Session.AuthenticationTicket); + actualSession.ClientIds.Should().BeEquivalentTo(sut.Session.AuthenticationTicket!.Properties.GetClientList()); } [Fact] @@ -63,7 +63,7 @@ public void ToUserSession_WhenIssuerSet_ShouldExtractIssuerInResult() actualSession.Renewed.Should().Be(sut.Session.Renewed); actualSession.Expires.Should().Be(sut.Session.Expires); actualSession.Issuer.Should().Be(fakeIssuer); - actualSession.AuthenticationTicket.Should().BeEquivalentTo(sut.AuthTicket); - actualSession.ClientIds.Should().BeEquivalentTo(sut.AuthTicket!.Properties.GetClientList()); + actualSession.AuthenticationTicket.Should().BeEquivalentTo(sut.Session.AuthenticationTicket); + actualSession.ClientIds.Should().BeEquivalentTo(sut.Session.AuthenticationTicket!.Properties.GetClientList()); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/IdentityServerMiddlewareTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/IdentityServerMiddlewareTests.cs index 2a05faaed..5d2c0486c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/IdentityServerMiddlewareTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/IdentityServerMiddlewareTests.cs @@ -282,9 +282,9 @@ public async Task Invoke_WhenSessionExpired_ShouldCallHandleUserSessionExpiry_On var ticketProperties = new AuthenticationProperties(); ticketProperties.AddClientId("client-1"); ticketProperties.AddClientId("client-2"); - var expiredTicket = new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity()), ticketProperties, "cookie"); + expiredSession.AuthenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity()), ticketProperties, "cookie"); - _context.SetSessionExpired(expiredSession, expiredTicket); + _context.SetSessionExpired(expiredSession); EndUserSessionEventContext actualContext = null; Mock.Get(userSessionEventsService) diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultSessionManagementServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultSessionManagementServiceTests.cs index e1c7f88fa..0123abded 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultSessionManagementServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultSessionManagementServiceTests.cs @@ -106,8 +106,8 @@ public async Task QuerySessionsAsync_WhenResultsReturned_ShouldMapToUserSessionC actualSession.Created.Should().Be(expected.Session.Created); actualSession.Renewed.Should().Be(expected.Session.Renewed); actualSession.Expires.Should().Be(expected.Session.Expires); - actualSession.AuthenticationTicket.Should().BeEquivalentTo(expected.AuthTicket); - actualSession.ClientIds.Should().BeEquivalentTo(expected.AuthTicket!.Properties.GetClientList()); + actualSession.AuthenticationTicket.Should().BeEquivalentTo(expected.Session.AuthenticationTicket); + actualSession.ClientIds.Should().BeEquivalentTo(expected.Session.AuthenticationTicket!.Properties.GetClientList()); } Mock.Get(serverSessionTicketStore) @@ -149,8 +149,8 @@ public async Task RemoveSessionsAsync_WhenFilterSpecified_ShouldRemoveAllSession .Verify(x => x.SendLogoutNotificationsAsync(It.Is(ctx => ctx.SessionId == fakeSession.Session.SessionId && ctx.SubjectId == fakeSession.Session.SubjectId && - fakeSession.AuthTicket != null && - ctx.ClientIds == fakeSession.AuthTicket.Properties.GetClientList()))); + fakeSession.Session.AuthenticationTicket != null && + ctx.ClientIds == fakeSession.Session.AuthenticationTicket.Properties.GetClientList()))); } Mock.Get(persistedGrantStore) @@ -230,8 +230,8 @@ public async Task RemoveSessionsAsync_WhenRemoveSessionsSetToFalse_ShouldNotRemo .Verify(x => x.SendLogoutNotificationsAsync(It.Is(ctx => ctx.SessionId == fakeSession.Session.SessionId && ctx.SubjectId == fakeSession.Session.SubjectId && - fakeSession.AuthTicket != null && - ctx.ClientIds.ToHashSet().SetEquals(fakeSession.AuthTicket.Properties.GetClientList())))); + fakeSession.Session.AuthenticationTicket != null && + ctx.ClientIds.ToHashSet().SetEquals(fakeSession.Session.AuthenticationTicket.Properties.GetClientList())))); } Mock.Get(persistedGrantStore) @@ -311,8 +311,8 @@ public async Task RemoveSessionsAsync_WhenRevokeTokensFalse_ShouldNotRevokeToken .Verify(x => x.SendLogoutNotificationsAsync(It.Is(ctx => ctx.SessionId == fakeSession.Session.SessionId && ctx.SubjectId == fakeSession.Session.SubjectId && - fakeSession.AuthTicket != null && - ctx.ClientIds.ToHashSet().SetEquals(fakeSession.AuthTicket.Properties.GetClientList())))); + fakeSession.Session.AuthenticationTicket != null && + ctx.ClientIds.ToHashSet().SetEquals(fakeSession.Session.AuthenticationTicket.Properties.GetClientList())))); } Mock.Get(persistedGrantStore) @@ -355,8 +355,8 @@ public async Task RemoveSessionsAsync_WhenRevokeConsentsFalse_ShouldNotRevokeCon .Verify(x => x.SendLogoutNotificationsAsync(It.Is(ctx => ctx.SessionId == fakeSession.Session.SessionId && ctx.SubjectId == fakeSession.Session.SubjectId && - fakeSession.AuthTicket != null && - ctx.ClientIds.ToHashSet().SetEquals(fakeSession.AuthTicket.Properties.GetClientList())))); + fakeSession.Session.AuthenticationTicket != null && + ctx.ClientIds.ToHashSet().SetEquals(fakeSession.Session.AuthenticationTicket.Properties.GetClientList())))); } Mock.Get(persistedGrantStore) @@ -399,8 +399,8 @@ public async Task RemoveSessionsAsync_WhenRevokeTokensAndConsentsFalse_ShouldNot .Verify(x => x.SendLogoutNotificationsAsync(It.Is(ctx => ctx.SessionId == fakeSession.Session.SessionId && ctx.SubjectId == fakeSession.Session.SubjectId && - fakeSession.AuthTicket != null && - ctx.ClientIds.ToHashSet().SetEquals(fakeSession.AuthTicket.Properties.GetClientList())))); + fakeSession.Session.AuthenticationTicket != null && + ctx.ClientIds.ToHashSet().SetEquals(fakeSession.Session.AuthenticationTicket.Properties.GetClientList())))); } Mock.Get(persistedGrantStore) diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionEventsServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionEventsServiceTests.cs index 89b404040..cd114afc2 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionEventsServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionEventsServiceTests.cs @@ -538,16 +538,14 @@ private async Task ValidateRefreshTokenAsync_CoordinationEnabledWithValidSession DateTime issued = fakeNow.AddDays(-10); DateTime expires = fakeNow.AddDays(19); - ServerSideSession fakeSession = FakeSession(fakKey, fakeScheme, fakeSessionId, fakeSubjectId, fakeDisplayName, + AuthenticationTicketFilterResult fakeSession = FakeSession(fakKey, fakeScheme, fakeSessionId, fakeSubjectId, fakeDisplayName, created: issued, renewed: issued, expires: expires); - AuthenticationTicket fakeAuthTicket = GenerateAuthenticationTicket(fakeScheme, fakeSubjectId, fakeSessionId, fakeDisplayName, + fakeSession.Session.AuthenticationTicket = GenerateAuthenticationTicket(fakeScheme, fakeSubjectId, fakeSessionId, fakeDisplayName, isPersistent: isPersistent, allowRefresh: allowRefresh, issuedUtc: issued, expiresUtc: expires); Mock.Get(serverSessionTicketStore) .Setup(x => x.FilterServerAuthenticationTickets(fakeSubjectId, fakeSessionId)) - .ReturnsAsync([ - new AuthenticationTicketFilterResult { Session = fakeSession, AuthTicket = fakeAuthTicket }, - ]); + .ReturnsAsync([fakeSession]); ServerSideSession? updatedSession = null; Mock.Get(identityServerServerSideSessionStore) @@ -558,8 +556,8 @@ private async Task ValidateRefreshTokenAsync_CoordinationEnabledWithValidSession bool actual = await sut.ValidateSession(testCtx); actual.Should().BeTrue(); - - updatedSession.Should().BeEquivalentTo(fakeSession, opt => opt + + updatedSession.Should().BeEquivalentTo(fakeSession.Session, opt => opt .Excluding(y => y.Renewed) .Excluding(y => y.Expires)); @@ -595,16 +593,14 @@ private async Task ValidateRefreshTokenAsync_CoordinationEnabledWithValidSession DateTime issued = fakeNow.AddDays(-10); DateTime expires = fakeNow.AddDays(19); - ServerSideSession fakeSession = FakeSession(fakKey, fakeScheme, fakeSessionId, fakeSubjectId, fakeDisplayName, + AuthenticationTicketFilterResult fakeSession = FakeSession(fakKey, fakeScheme, fakeSessionId, fakeSubjectId, fakeDisplayName, created: issued, renewed: issued, expires: expires); - AuthenticationTicket fakeAuthTicket = GenerateAuthenticationTicket(fakeScheme, fakeSubjectId, fakeSessionId, fakeDisplayName, + fakeSession.Session.AuthenticationTicket = GenerateAuthenticationTicket(fakeScheme, fakeSubjectId, fakeSessionId, fakeDisplayName, isPersistent: isPersistent, allowRefresh: allowRefresh, issuedUtc: issued, expiresUtc: expires); Mock.Get(serverSessionTicketStore) .Setup(x => x.FilterServerAuthenticationTickets(fakeSubjectId, fakeSessionId)) - .ReturnsAsync([ - new AuthenticationTicketFilterResult { Session = fakeSession, AuthTicket = fakeAuthTicket }, - ]); + .ReturnsAsync([fakeSession]); ServerSideSession? updatedSession = null; Mock.Get(identityServerServerSideSessionStore) @@ -616,7 +612,7 @@ private async Task ValidateRefreshTokenAsync_CoordinationEnabledWithValidSession actual.Should().BeTrue(); - updatedSession.Should().BeEquivalentTo(fakeSession, opt => opt + updatedSession.Should().BeEquivalentTo(fakeSession.Session, opt => opt .Excluding(y => y.Renewed) .Excluding(y => y.Expires)); @@ -652,16 +648,14 @@ private async Task ValidateRefreshTokenAsync_CoordinationEnabledWithValidSession DateTime issued = fakeNow.AddDays(-10); DateTime expires = fakeNow.AddDays(19); - ServerSideSession fakeSession = FakeSession(fakKey, fakeScheme, fakeSessionId, fakeSubjectId, fakeDisplayName, + AuthenticationTicketFilterResult fakeSession = FakeSession(fakKey, fakeScheme, fakeSessionId, fakeSubjectId, fakeDisplayName, created: issued, renewed: issued, expires: expires); - AuthenticationTicket fakeAuthTicket = GenerateAuthenticationTicket(fakeScheme, fakeSubjectId, fakeSessionId, fakeDisplayName, + fakeSession.Session.AuthenticationTicket = GenerateAuthenticationTicket(fakeScheme, fakeSubjectId, fakeSessionId, fakeDisplayName, isPersistent: isPersistent, allowRefresh: allowRefresh, issuedUtc: issued, expiresUtc: expires); Mock.Get(serverSessionTicketStore) .Setup(x => x.FilterServerAuthenticationTickets(fakeSubjectId, fakeSessionId)) - .ReturnsAsync([ - new AuthenticationTicketFilterResult { Session = fakeSession, AuthTicket = fakeAuthTicket }, - ]); + .ReturnsAsync([fakeSession]); AuthenticationTicket? updatedTicket = null; Mock.Get(serverSessionTicketStore) @@ -673,7 +667,7 @@ private async Task ValidateRefreshTokenAsync_CoordinationEnabledWithValidSession actual.Should().BeTrue(); - updatedTicket.Should().BeEquivalentTo(fakeAuthTicket); + updatedTicket.Should().BeEquivalentTo(fakeSession.Session.AuthenticationTicket); updatedTicket.Properties.IssuedUtc.Should().Be(fakeNow); updatedTicket.Properties.ExpiresUtc.Should().Be(fakeNow.AddDays(29)); updatedTicket.Properties.GetString(IdentityServerConstants.ForceCookieRefresh).Should().BeEmpty(); @@ -706,18 +700,18 @@ public async Task PublicMethods_WhenCalled_ShouldTelemetryTrace() foreach ((Func actMethod, string traceMethodName) method in methods) { - ITrace trace = Mock.Of(); + ITrace traceMock = Mock.Of(); Mock.Get(telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(trace); - Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); - Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + .Returns(traceMock); + Mock.Get(traceMock).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(traceMock); + Mock.Get(traceMock).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(traceMock); await method.actMethod(sut); Mock.Get(telemetry) .Verify(t => t.Trace( TelemetryConstants.TraceCategories.Services, sut, method.traceMethodName), Times.Once); - Mock.Get(trace).Verify(t => t.Dispose(), Times.Once); + Mock.Get(traceMock).Verify(t => t.Dispose(), Times.Once); } // Assert all methods covered @@ -758,7 +752,7 @@ private AuthenticationTicket GenerateAuthenticationTicket(string authScheme, str return new AuthenticationTicket(user.CreatePrincipal(), properties, authScheme); } - private ServerSideSession FakeSession( + private AuthenticationTicketFilterResult FakeSession( string key, string scheme, string sessionId, @@ -769,12 +763,15 @@ private ServerSideSession FakeSession( DateTime? renewed = null, DateTime? expires = null) { - return new ServerSideSession + return new AuthenticationTicketFilterResult { - Key = key, Scheme = scheme, SessionId = sessionId, SubjectId = subjectId, DisplayName = displayName, Data = data ?? string.Empty, - Created = created ?? new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc), - Renewed = renewed ?? new DateTime(2026, 1, 2, 12, 0, 0, DateTimeKind.Utc), - Expires = expires ?? new DateTime(2026, 1, 31, 12, 0, 0, DateTimeKind.Utc), + Session = new ServerSideSession + { + Key = key, Scheme = scheme, SessionId = sessionId, SubjectId = subjectId, DisplayName = displayName, Data = data ?? string.Empty, + Created = created ?? new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc), + Renewed = renewed ?? new DateTime(2026, 1, 2, 12, 0, 0, DateTimeKind.Utc), + Expires = expires ?? new DateTime(2026, 1, 31, 12, 0, 0, DateTimeKind.Utc), + }, }; } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/SessionCleanupServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/SessionCleanupServiceTests.cs index 51944daac..3fb8570e6 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/SessionCleanupServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/SessionCleanupServiceTests.cs @@ -15,7 +15,6 @@ using Open.IdentityServer.Extensions; using Open.IdentityServer.Models; using Open.IdentityServer.Services; -using Open.IdentityServer.Services.Default; using Open.IdentityServer.Stores; using Xunit; @@ -195,11 +194,9 @@ private static AuthenticationTicketFilterResult FakeSessionSession(string subjec session.Expires = DateTime.UtcNow.AddDays(-3); } - return new AuthenticationTicketFilterResult - { - Session = session, - AuthTicket = GenerateAuthenticationTicket(session, clientsIds), - }; + session.AuthenticationTicket = GenerateAuthenticationTicket(session, clientsIds); + + return new AuthenticationTicketFilterResult { Session = session }; } private static AuthenticationTicket GenerateAuthenticationTicket(ServerSideSession session, string[]? clientIds = null) diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/ServerSessionTicketStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/ServerSessionTicketStoreTests.cs index 9612fdd80..477472c16 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/ServerSessionTicketStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/ServerSessionTicketStoreTests.cs @@ -6,22 +6,18 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AwesomeAssertions; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Time.Testing; using Moq; using Open.IdentityServer.Configuration; -using Open.IdentityServer.DataProtection; using Open.IdentityServer.Extensions; using Open.IdentityServer.Models; using Open.IdentityServer.Services; using Open.IdentityServer.Stores; -using Open.IdentityServer.Stores.Serialization; using Open.IdentityServer.Test.Utilities; using Open.IdentityServer.UnitTests.Utilities.Generators; using Xunit; @@ -36,16 +32,14 @@ public class ServerSessionTicketStoreTests private readonly IPersistedGrantStore persistedGrantStore = Mock.Of(); - private readonly IDataProtectionProvider dataProtectionProvider = Mock.Of(); private readonly IHandleGenerationService handleGenerationService = Mock.Of(); - private readonly MockDataProtector dataProtector = new(); private readonly FakeTimeProvider fakeTimeProvider = new(); private readonly IHttpContextAccessor httpContextAccessor = Mock.Of(); private readonly ITelemetryService telemetry = Mock.Of(); private readonly MockLogger logger = new(); private readonly IdentityServerOptions fakeOptions = new(); - private readonly IServiceProvider fakeserviceProvider = Mock.Of(); + private readonly IServiceProvider fakeServiceProvider = Mock.Of(); private readonly HttpContext fakeHttpContext = new DefaultHttpContext(); private static readonly DateTime FakeNow = new(2026, 01, 01, 12, 0, 0, DateTimeKind.Utc); @@ -55,11 +49,7 @@ public ServerSessionTicketStoreTests() { fakeTimeProvider.SetUtcNow(FakeNow); fakeOptions.IssuerUri = FakeIssuer; - fakeHttpContext.RequestServices = fakeserviceProvider; - - Mock.Get(dataProtectionProvider) - .Setup(x => x.CreateProtector(DataProtectionConstants.ServerSideTicketStorePurpose)) - .Returns(dataProtector); + fakeHttpContext.RequestServices = fakeServiceProvider; Mock.Get(handleGenerationService) .Setup(x => x.GenerateAsync(32)) @@ -69,7 +59,7 @@ public ServerSessionTicketStoreTests() .Setup(x => x.FilterSessions(It.IsAny())) .ReturnsAsync(QueryResult.Empty); - Mock.Get(fakeserviceProvider) + Mock.Get(fakeServiceProvider) .Setup(x => x.GetService(typeof(IdentityServerOptions))) .Returns(fakeOptions); @@ -79,8 +69,7 @@ public ServerSessionTicketStoreTests() } private ServerSessionTicketStore CreateSut() => new( - serverServerSideSessionStore, - dataProtectionProvider, + serverServerSideSessionStore, persistedGrantStore, handleGenerationService, fakeTimeProvider, fakeOptions, httpContextAccessor, telemetry, logger); @@ -115,19 +104,7 @@ public async Task StoreAsync_WhenOptionalValuesNotProvided_ShouldUseCorrectDefau createdSessionModel.Created.Should().Be(FakeNow); createdSessionModel.Renewed.Should().Be(FakeNow); createdSessionModel.Expires.Should().BeNull(); - - var jsonElement = JsonElement.Parse(createdSessionModel.Data); - - jsonElement.GetProperty("Version").GetInt32().Should().Be(1); - var actualPayload = jsonElement.GetProperty("Payload").GetString(); - actualPayload.Should().NotBeNull(); - - var expectedAuthTicket = authenticationTicket.Clone(); - expectedAuthTicket.Properties.Items[JwtClaimTypes.Issuer] = FakeIssuer; - - string expectedJson = JsonSerializer.Serialize(expectedAuthTicket.ToSerializableObj(), - ServerSessionTicketStore.JsonSettings); - dataProtector.ValidateProtectedData(actualPayload, expectedJson); + createdSessionModel.AuthenticationTicket.Should().Be(authenticationTicket); } [Fact] @@ -164,19 +141,7 @@ public async Task StoreAsync_WhenOptionalValuesProvided_ShouldUseThem() createdSessionModel.Created.Should().Be(issuedUtc); createdSessionModel.Renewed.Should().Be(issuedUtc); createdSessionModel.Expires.Should().Be(expiresUtc); - - var jsonElement = JsonElement.Parse(createdSessionModel.Data); - - jsonElement.GetProperty("Version").GetInt32().Should().Be(1); - var actualPayload = jsonElement.GetProperty("Payload").GetString(); - actualPayload.Should().NotBeNull(); - - var expectedAuthTicket = authenticationTicket.Clone(); - expectedAuthTicket.Properties.Items[JwtClaimTypes.Issuer] = FakeIssuer; - - string expectedJson = JsonSerializer.Serialize(expectedAuthTicket.ToSerializableObj(), - ServerSessionTicketStore.JsonSettings); - dataProtector.ValidateProtectedData(actualPayload, expectedJson); + createdSessionModel.AuthenticationTicket.Should().Be(authenticationTicket); } [Theory] @@ -217,19 +182,7 @@ public async Task StoreAsync_WhenDisplayNameClaimSet_ShouldUseClaimValueIfSet(st createdSessionModel.Created.Should().Be(issuedUtc); createdSessionModel.Renewed.Should().Be(issuedUtc); createdSessionModel.Expires.Should().Be(expiresUtc); - - var jsonElement = JsonElement.Parse(createdSessionModel.Data); - - jsonElement.GetProperty("Version").GetInt32().Should().Be(1); - var actualPayload = jsonElement.GetProperty("Payload").GetString(); - actualPayload.Should().NotBeNull(); - - var expectedAuthTicket = authenticationTicket.Clone(); - expectedAuthTicket.Properties.Items[JwtClaimTypes.Issuer] = FakeIssuer; - - string expectedJson = JsonSerializer.Serialize(expectedAuthTicket.ToSerializableObj(), - ServerSessionTicketStore.JsonSettings); - dataProtector.ValidateProtectedData(actualPayload, expectedJson); + createdSessionModel.AuthenticationTicket.Should().Be(authenticationTicket); } [Fact] @@ -274,22 +227,12 @@ public async Task RenewAsync_WhenOptionalValuesNotProvided_ShouldUseCorrectDefau createdSessionModel.Created.Should().Be(existingSession.Created); createdSessionModel.Renewed.Should().Be(FakeNow); createdSessionModel.Expires.Should().BeNull(); - - var jsonElement = JsonElement.Parse(createdSessionModel.Data); - - jsonElement.GetProperty("Version").GetInt32().Should().Be(1); - var actualPayload = jsonElement.GetProperty("Payload").GetString(); - actualPayload.Should().NotBeNull(); - - string expectedJson = JsonSerializer.Serialize(authenticationTicket.ToSerializableObj(), - ServerSessionTicketStore.JsonSettings); - dataProtector.ValidateProtectedData(actualPayload, expectedJson); + createdSessionModel.AuthenticationTicket.Should().Be(authenticationTicket); } [Fact] public async Task RenewAsync_WhenSessionOverwritten_ShouldExpirePreviousGrants() { - ServerSideSession existingSession = new ServerSideSession { Key = Guid.NewGuid().ToString(), Scheme = "AuthScheme", @@ -357,7 +300,7 @@ public async Task RenewAsync_WhenOptionalValuesProvided_ShouldUseThem() AuthenticationTicket authenticationTicket = ServerSessionTestGenerators.GenerateAuthenticationTicket(authScheme, subjectId, sessionId, displayName, issuedUtc, expiresUtc); - + Mock.Get(serverServerSideSessionStore) .Setup(x => x.GetSession(existingSession.Key)) .ReturnsAsync(existingSession); @@ -381,16 +324,7 @@ public async Task RenewAsync_WhenOptionalValuesProvided_ShouldUseThem() updatedSessionModel.Created.Should().Be(existingSession.Created); updatedSessionModel.Renewed.Should().Be(issuedUtc); updatedSessionModel.Expires.Should().Be(expiresUtc); - - var jsonElement = JsonElement.Parse(updatedSessionModel.Data); - - jsonElement.GetProperty("Version").GetInt32().Should().Be(1); - var actualPayload = jsonElement.GetProperty("Payload").GetString(); - actualPayload.Should().NotBeNull(); - - string expectedJson = JsonSerializer.Serialize(authenticationTicket.ToSerializableObj(), - ServerSessionTicketStore.JsonSettings); - dataProtector.ValidateProtectedData(actualPayload, expectedJson); + updatedSessionModel.AuthenticationTicket.Should().Be(authenticationTicket); } [Fact] @@ -401,7 +335,7 @@ public async Task RenewAsync_WhenNoExistingSessionWithKey_ShouldCreateNewSession string sessionId = Guid.NewGuid().ToString(); AuthenticationTicket authenticationTicket = ServerSessionTestGenerators.GenerateAuthenticationTicket(authScheme, subjectId, sessionId); - + ServerSideSession? createdSessionModel = null; Mock.Get(serverServerSideSessionStore) .Setup(x => x.CreateSession(It.IsAny())) @@ -422,19 +356,7 @@ public async Task RenewAsync_WhenNoExistingSessionWithKey_ShouldCreateNewSession createdSessionModel.Created.Should().Be(FakeNow); createdSessionModel.Renewed.Should().Be(FakeNow); createdSessionModel.Expires.Should().BeNull(); - - var jsonElement = JsonElement.Parse(createdSessionModel.Data); - - jsonElement.GetProperty("Version").GetInt32().Should().Be(1); - var actualPayload = jsonElement.GetProperty("Payload").GetString(); - actualPayload.Should().NotBeNull(); - - var expectedAuthTicket = authenticationTicket.Clone(); - expectedAuthTicket.Properties.Items[JwtClaimTypes.Issuer] = FakeIssuer; - - string expectedJson = JsonSerializer.Serialize(expectedAuthTicket.ToSerializableObj(), - ServerSessionTicketStore.JsonSettings); - dataProtector.ValidateProtectedData(actualPayload, expectedJson); + createdSessionModel.AuthenticationTicket.Should().Be(authenticationTicket); } [Theory] @@ -496,10 +418,9 @@ public async Task RetrieveAsync_WhenSessionStoredForKey_ShouldReturnDeserialized Renewed = new DateTime(2026, 1, 2, 12, 0, 0, DateTimeKind.Utc), Expires = new DateTime(2026, 1, 31, 12, 0, 0, DateTimeKind.Utc), }; - SerializedAuthenticationTicket authenticationTicket = ServerSessionTestGenerators.GenerateSerializedAuthenticationTicket( + existingSession.AuthenticationTicket = ServerSessionTestGenerators.GenerateAuthenticationTicket( existingSession.Scheme, existingSession.SubjectId, existingSession.SessionId, existingSession.DisplayName, existingSession.Renewed, existingSession.Expires); - existingSession.Data = GenerateFakeProtectedData(authenticationTicket); Mock.Get(serverServerSideSessionStore) .Setup(x => x.GetSession(existingSession.Key)) @@ -508,16 +429,7 @@ public async Task RetrieveAsync_WhenSessionStoredForKey_ShouldReturnDeserialized ServerSessionTicketStore sut = CreateSut(); AuthenticationTicket? actual = await sut.RetrieveAsync(existingSession.Key); - actual.Should().BeOfType(); - actual.AuthenticationScheme.Should().Be(existingSession.Scheme); - actual.Principal.Identity.Should().NotBeNull(); - actual.Principal.Identity?.AuthenticationType.Should() - .BeEquivalentTo(authenticationTicket.User.AuthenticationType); - actual.Principal.Identity?.Name.Should().BeEquivalentTo(existingSession.DisplayName); - actual.Principal.Identities.Should().Contain(x => - x.NameClaimType == JwtClaimTypes.Name && - x.RoleClaimType == JwtClaimTypes.Role); - actual.Properties.Items.Should().BeEquivalentTo(authenticationTicket.Items); + actual.Should().BeEquivalentTo(existingSession.AuthenticationTicket); } [Theory] @@ -556,15 +468,16 @@ public async Task RemoveAsync_WhenSessionStillExists_ShouldStoreExpiredSessionAn Created = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc), Renewed = new DateTime(2026, 1, 2, 12, 0, 0, DateTimeKind.Utc), Expires = new DateTime(2026, 1, 31, 12, 0, 0, DateTimeKind.Utc), - Data = GenerateFakeProtectedData(ServerSessionTestGenerators.GenerateSerializedAuthenticationTicket( - "AuthScheme", - "bob", - "session_id", - "bob", - new DateTime(2026, 1, 2, 12, 0, 0, DateTimeKind.Utc), - new DateTime(2026, 1, 31, 12, 0, 0, DateTimeKind.Utc), - ["1", "2"])) + Data = Guid.NewGuid().ToString(), }; + existingSession.AuthenticationTicket = ServerSessionTestGenerators.GenerateAuthenticationTicket( + "AuthScheme", + "bob", + "session_id", + "bob", + new DateTime(2026, 1, 2, 12, 0, 0, DateTimeKind.Utc), + new DateTime(2026, 1, 31, 12, 0, 0, DateTimeKind.Utc), + clientIds: ["1", "2"]); Mock.Get(serverServerSideSessionStore) .Setup(x => x.GetSession(keyId)) @@ -580,42 +493,13 @@ public async Task RemoveAsync_WhenSessionStillExists_ShouldStoreExpiredSessionAn var expiredTicket = fakeHttpContext.Items["idsvr:TicketExpired"] as AuthenticationTicket; expiredTicket.Should().NotBeNull(); - expiredTicket!.Principal.GetSubjectId().Should().Be("bob"); + expiredTicket.Principal.GetSubjectId().Should().Be("bob"); expiredTicket.Properties.GetSessionId().Should().Be("session_id"); Mock.Get(serverServerSideSessionStore) .Verify(x => x.DeleteSession(keyId)); } - [Theory] - [InlineData("{invalid.json}")] - [InlineData("{\"Version\": 2, Payload: \"SOMEDATA\"}")] - public async Task FilterServerAuthenticationTickets_WhenSessionDataDeserialisationFails_ShouldReturnSkipFailingAuthTicket(string data) - { - const string testSubjectId = "bob"; - const string testSessionId = "session-0"; - - IEnumerable sessions = [ - ServerSessionTestGenerators.FakeSession(key: "key-0", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ServerSessionTestGenerators.FakeSession(key: "key-4", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: data), - ServerSessionTestGenerators.FakeSession(key: "key-5", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ]; - - List expectedAuthTickets = []; - sessions = sessions.Select(session => GenerateSerialisedData(expectedAuthTickets, session)); - - Mock.Get(serverServerSideSessionStore) - .Setup(x => x.FilterSessions(testSubjectId, testSessionId)) - .ReturnsAsync(sessions); - - ServerSessionTicketStore sut = CreateSut(); - IEnumerable actual = - (await sut.FilterServerAuthenticationTickets(testSubjectId, testSessionId)).ToList(); - - actual.Should().NotBeNullOrEmpty(); - actual.Should().HaveCount(expectedAuthTickets.Count); - } - [Fact] public async Task FilterServerAuthenticationTickets_WhenNotingReturnedByStore_ShouldReturnEmptyCollection() { @@ -638,31 +522,22 @@ public async Task FilterServerAuthenticationTickets_WhenSessionReturnedFromStore const string testSubjectId = "bob"; const string testSessionId = "session-0"; - IEnumerable sessions = [ - ServerSessionTestGenerators.FakeSession(key: "key-0", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ServerSessionTestGenerators.FakeSession(key: "key-4", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ServerSessionTestGenerators.FakeSession(key: "key-5", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), + List sessions = [ + ServerSessionTestGenerators.FakeSession(key: "key-0", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), + ServerSessionTestGenerators.FakeSession(key: "key-4", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), + ServerSessionTestGenerators.FakeSession(key: "key-5", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), ]; - - List expectedAuthTickets = []; - sessions = sessions.Select(x => GenerateSerialisedData(expectedAuthTickets, x)) - .ToList(); Mock.Get(serverServerSideSessionStore) .Setup(x => x.FilterSessions(testSubjectId, testSessionId)) .ReturnsAsync(sessions); ServerSessionTicketStore sut = CreateSut(); - IEnumerable actual = - (await sut.FilterServerAuthenticationTickets(testSubjectId, testSessionId)).ToList(); - - actual.Should().NotBeNullOrEmpty(); - actual.Should().HaveCount(expectedAuthTickets.Count); + IEnumerable actual = [.. await sut.FilterServerAuthenticationTickets(testSubjectId, testSessionId)]; - foreach (var session in sessions) - { - ValidateAutTicketExists(actual, session); - } + List expected = [.. sessions.Select(x => new AuthenticationTicketFilterResult { Session = x })]; + + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -670,40 +545,22 @@ public async Task GetAndRemoveExpiredSessions_WhenSessionReturnedFromStore_Shoul { const int batchSize = 5; - IEnumerable sessions = [ - ServerSessionTestGenerators.FakeSession(key: "key-0", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ServerSessionTestGenerators.FakeSession(key: "key-4", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ServerSessionTestGenerators.FakeSession(key: "key-5", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), + List sessions = [ + ServerSessionTestGenerators.FakeSession(key: "key-0", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), + ServerSessionTestGenerators.FakeSession(key: "key-4", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), + ServerSessionTestGenerators.FakeSession(key: "key-5", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), ]; - - List expectedAuthTickets = []; - sessions = sessions.Select(x => GenerateSerialisedData(expectedAuthTickets, x)) - .ToList(); Mock.Get(serverServerSideSessionStore) .Setup(x => x.GetAndRemoveExpiredSessions(batchSize)) .ReturnsAsync(sessions); ServerSessionTicketStore sut = CreateSut(); - IEnumerable actual = - (await sut.GetAndRemoveExpiredSessions(batchSize)).ToList(); + IEnumerable actual = [.. await sut.GetAndRemoveExpiredSessions(batchSize)]; - actual.Should().NotBeNullOrEmpty(); - actual.Should().HaveCount(expectedAuthTickets.Count); - - foreach (var session in sessions) - { - ValidateAutTicketExists(actual, session); - } - } + List expected = [.. sessions.Select(x => new AuthenticationTicketFilterResult { Session = x })]; - private void ValidateAutTicketExists(IEnumerable result, ServerSideSession sesison) - { - result.Should().Contain(x => - x.AuthTicket != null && - x.AuthTicket.AuthenticationScheme == sesison.Scheme && - x.AuthTicket.Principal.GetSubjectId() == sesison.SubjectId && - x.AuthTicket.Properties.GetSessionId() == sesison.SessionId); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -716,17 +573,15 @@ public async Task FilterServerAuthenticationTickets_WhenQueryProvided_ShouldCall }; QueryResult fakeResult; - IEnumerable sessions = [ - ServerSessionTestGenerators.FakeSession(key: "key-0", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ServerSessionTestGenerators.FakeSession(key: "key-4", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), - ServerSessionTestGenerators.FakeSession(key: "key-5", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith"), + List sessions = [ + ServerSessionTestGenerators.FakeSession(key: "key-0", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), + ServerSessionTestGenerators.FakeSession(key: "key-4", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), + ServerSessionTestGenerators.FakeSession(key: "key-5", scheme: "AuthScheme", subjectId: "bob", sessionId: "session-0", displayName: "Bob Smith", data: Guid.NewGuid().ToString()), ]; - - List expectedAuthTickets = []; + fakeResult = new QueryResult { - - Results = sessions.Select(x => GenerateSerialisedData(expectedAuthTickets, x)).ToList(), + Results = sessions, }; Mock.Get(serverServerSideSessionStore) @@ -734,13 +589,21 @@ public async Task FilterServerAuthenticationTickets_WhenQueryProvided_ShouldCall .ReturnsAsync(fakeResult); ServerSessionTicketStore sut = CreateSut(); - QueryResult actual = - (await sut.FilterServerAuthenticationTickets(fakeQuery, TestContext.Current.CancellationToken)); + QueryResult actual = await sut.FilterServerAuthenticationTickets(fakeQuery, TestContext.Current.CancellationToken); + + var expected = new QueryResult + { + ResultsToken = fakeResult.ResultsToken, + HasPrevResults = fakeResult.HasPrevResults, + HasNextResults = fakeResult.HasNextResults, + TotalCount = fakeResult.TotalCount, + TotalPages = fakeResult.TotalPages, + CurrentPage = fakeResult.CurrentPage, + Results = [.. fakeResult.Results.Select(x => new AuthenticationTicketFilterResult { Session = x })], + }; actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(fakeResult, cnf => cnf.Excluding(x => x.Results)); - actual.Results.Should().NotBeNullOrEmpty(); - actual.Results.Should().HaveCount(expectedAuthTickets.Count); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -786,31 +649,22 @@ public async Task PublicMethods_WhenCalled_ShouldTelemetryTrace() .Should().BeEquivalentTo(methods.Select(m => m.traceMethodName)); } - private ServerSideSession GenerateSerialisedData( - List expectedAuthTickets, - ServerSideSession ServerSideSession) - { - if (string.IsNullOrWhiteSpace(ServerSideSession.Data)) - { - SerializedAuthenticationTicket authenticationTicket = ServerSessionTestGenerators.GenerateSerializedAuthenticationTicket( - ServerSideSession.Scheme, ServerSideSession.SubjectId, ServerSideSession.SessionId, - ServerSideSession.DisplayName, ServerSideSession.Renewed, ServerSideSession.Expires); - ServerSideSession.Data = GenerateFakeProtectedData(authenticationTicket); - - expectedAuthTickets.Add(authenticationTicket); - } - - return ServerSideSession; - } - - private string GenerateFakeProtectedData(SerializedAuthenticationTicket serializedAuthenticationTicket) - { - DataProtectedSessionData sessionData = new DataProtectedSessionData - { - Payload = dataProtector.GenerateFakeProtectedData(JsonSerializer.Serialize(serializedAuthenticationTicket, - ServerSessionTicketStore.JsonSettings)) - }; - - return JsonSerializer.Serialize(sessionData, ServerSessionTicketStore.JsonSettings); - } + // private List SetupAuthTicketDeserialisation(IEnumerable sessions) + // { + // List expectedTickets = []; + // + // foreach (var session in sessions) + // { + // var ticket = ServerSessionTestGenerators.GenerateAuthenticationTicket(session.Scheme, session.SubjectId, + // session.SessionId, session.DisplayName, session.Renewed, session.Expires); + // + // Mock.Get(authenticationTicketSerializer) + // .Setup(x => x.Deserialize(session.Data)) + // .Returns(ticket); + // + // expectedTickets.Add(ticket); + // } + // + // return expectedTickets; + // } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Utilities/Generators/ServerSessionTestGenerators.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Utilities/Generators/ServerSessionTestGenerators.cs index 1b421c663..903a26aef 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Utilities/Generators/ServerSessionTestGenerators.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Utilities/Generators/ServerSessionTestGenerators.cs @@ -52,6 +52,7 @@ public static SerializedAuthenticationTicket GenerateSerializedAuthenticationTic string? displayName = null, DateTimeOffset? issuedUtc = null, DateTimeOffset? expiresUtc = null, + string? issuer = null, List? clientIds = null) { List claims = []; @@ -82,6 +83,11 @@ public static SerializedAuthenticationTicket GenerateSerializedAuthenticationTic { items[".expires"] = expiresUtc.Value.ToString("R"); } + + if (issuer != null) + { + items[JwtClaimTypes.Issuer] = issuer; + } if (clientIds is { Count: > 0 }) { @@ -136,11 +142,12 @@ public static AuthenticationTicketFilterResult GenerateAuthenticationTicketFilte created ??= new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc); renewed ??= new DateTime(2026, 1, 2, 12, 0, 0, DateTimeKind.Utc); expires ??= new DateTime(2026, 1, 31, 12, 0, 0, DateTimeKind.Utc); + + ServerSideSession session = FakeSession(key, authScheme, sessionId, subjectId, displayName, string.Empty, created, renewed, expires); + + session.AuthenticationTicket = GenerateAuthenticationTicket(authScheme, subjectId, sessionId, displayName, + renewed, expires, issuer, clientIds); - return new AuthenticationTicketFilterResult - { - Session = FakeSession(key, authScheme, sessionId, subjectId, displayName, string.Empty, created, renewed, expires), - AuthTicket = GenerateAuthenticationTicket(authScheme, subjectId, sessionId, displayName, renewed, expires, issuer, clientIds), - }; + return new AuthenticationTicketFilterResult { Session = session }; } } \ No newline at end of file diff --git a/src/Storage/src/Models/ServerSideSession.cs b/src/Storage/src/Models/ServerSideSession.cs index 1e8e4d6a8..cf15c9edf 100644 --- a/src/Storage/src/Models/ServerSideSession.cs +++ b/src/Storage/src/Models/ServerSideSession.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using Microsoft.AspNetCore.Authentication; namespace Open.IdentityServer.Models; @@ -56,4 +57,9 @@ public class ServerSideSession /// Get or set data value /// public string Data { get; set; } = null!; + + /// + /// Get or set data value + /// + public AuthenticationTicket? AuthenticationTicket { get; set; } } \ No newline at end of file diff --git a/src/Storage/src/Open.IdentityServer.Storage.csproj b/src/Storage/src/Open.IdentityServer.Storage.csproj index 2eef219c6..a976d97e4 100644 --- a/src/Storage/src/Open.IdentityServer.Storage.csproj +++ b/src/Storage/src/Open.IdentityServer.Storage.csproj @@ -38,6 +38,7 @@ + \ No newline at end of file diff --git a/src/Storage/src/Stores/Serialization/IAuthenticationTicketSerializer.cs b/src/Storage/src/Stores/Serialization/IAuthenticationTicketSerializer.cs new file mode 100644 index 000000000..a16a52247 --- /dev/null +++ b/src/Storage/src/Stores/Serialization/IAuthenticationTicketSerializer.cs @@ -0,0 +1,25 @@ +#nullable enable + +using Microsoft.AspNetCore.Authentication; + +namespace Open.IdentityServer.Stores.Serialization; + +/// +/// Interface defining an authentication ticket serializer +/// +public interface IAuthenticationTicketSerializer +{ + /// + /// Serializes the object passed in into a string + /// + /// ticket to be serialized + /// string contaimng data from ticket serialized + string Serialize(AuthenticationTicket ticket); + + /// + /// Deserializes a string into a object + /// + /// data to be deserialized + /// ticket deserialized from input data string + AuthenticationTicket? Deserialize(string data); +} \ No newline at end of file