diff --git a/src/EntityFramework.Storage/src/Mappers/ClientMappingExtensions.cs b/src/EntityFramework.Storage/src/Mappers/ClientMappingExtensions.cs index ac9d823f..bc650c8e 100644 --- a/src/EntityFramework.Storage/src/Mappers/ClientMappingExtensions.cs +++ b/src/EntityFramework.Storage/src/Mappers/ClientMappingExtensions.cs @@ -77,6 +77,7 @@ public Models.Client ToModel() DeviceCodeLifetime = clientEntity.DeviceCodeLifetime, AllowedCorsOrigins = clientEntity.AllowedCorsOrigins.ToStringCollection(), Properties = clientEntity.Properties.ToModelDictionary(), + PollingInterval = clientEntity.PollingInterval, }; } } @@ -181,6 +182,7 @@ public Entities.Client ToEntity() DeviceCodeLifetime = clientModel.DeviceCodeLifetime, AllowedCorsOrigins = clientModel.AllowedCorsOrigins?.Select(x => new ClientCorsOrigin { Origin = x }).ToList() ?? [], Properties = clientModel.Properties.ToEntityList(), + PollingInterval = clientModel.PollingInterval, }; } } diff --git a/src/EntityFramework.Storage/test/UnitTests/Mappers/ClientMappersTests.cs b/src/EntityFramework.Storage/test/UnitTests/Mappers/ClientMappersTests.cs index c419b630..50e1fd4e 100644 --- a/src/EntityFramework.Storage/test/UnitTests/Mappers/ClientMappersTests.cs +++ b/src/EntityFramework.Storage/test/UnitTests/Mappers/ClientMappersTests.cs @@ -111,7 +111,6 @@ public void ToEntity_maps_all_properties() nameof(Entities.Client.NonEditable), // Compatibility properties intentionally not mapped nameof(Entities.Client.CibaLifetime), - nameof(Entities.Client.PollingInterval), nameof(Entities.Client.CoordinateLifetimeWithUserSession), nameof(Entities.Client.InitiateLoginUri), nameof(Entities.Client.DPoPClockSkew), @@ -129,7 +128,6 @@ public void ToModel_maps_all_properties() .ExcludeDestinationProperties( // Compatibility properties intentionally not mapped nameof(Client.CibaLifetime), - nameof(Client.PollingInterval), nameof(Client.CoordinateLifetimeWithUserSession), nameof(Client.InitiateLoginUri), nameof(Client.DPoPClockSkew), diff --git a/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs b/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs index c619901c..b4d55176 100644 --- a/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs @@ -3,11 +3,12 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -using System; -using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; -using Microsoft.Extensions.Caching.Distributed; +using Open.IdentityServer.Stores; +using System; +using System.Threading.Tasks; namespace Open.IdentityServer.Services; @@ -18,6 +19,7 @@ namespace Open.IdentityServer.Services; public class DistributedDeviceFlowThrottlingService : IDeviceFlowThrottlingService { private readonly IDistributedCache _cache; + private readonly IClientStore _clientStore; private readonly TimeProvider _clock; private readonly IdentityServerOptions _options; private readonly ITelemetryService _telemetry; @@ -28,16 +30,19 @@ public class DistributedDeviceFlowThrottlingService : IDeviceFlowThrottlingServi /// Initializes a new instance of the class. /// /// The cache. + /// The client store. /// The clock. /// The options. /// The telemetry public DistributedDeviceFlowThrottlingService( IDistributedCache cache, + IClientStore clientStore, TimeProvider clock, IdentityServerOptions options, ITelemetryService telemetry) { _cache = cache; + _clientStore = clientStore; _clock = clock; _options = options; _telemetry = telemetry; @@ -70,7 +75,8 @@ public async Task ShouldSlowDown(string deviceCode, DeviceCode details) // check interval if (DateTime.TryParse(lastSeenAsString, out var lastSeen)) { - if (_clock.GetUtcNow() < lastSeen.AddSeconds(_options.DeviceFlow.Interval)) + var client = await _clientStore.FindEnabledClientByIdAsync(details.ClientId); + if (_clock.GetUtcNow() < lastSeen.AddSeconds(client?.PollingInterval ?? _options.DeviceFlow.Interval)) { await _cache.SetStringAsync(key, _clock.GetUtcNow().ToString("O"), options); return true; diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs index 3f84c604..beb743ca 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs @@ -2,18 +2,19 @@ // Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using AwesomeAssertions; +using Microsoft.Extensions.Caching.Distributed; +using Moq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Tasks; -using AwesomeAssertions; -using Open.IdentityServer.UnitTests.Common; -using Open.IdentityServer.Configuration; -using Open.IdentityServer.Models; -using Open.IdentityServer.Services; -using Microsoft.Extensions.Caching.Distributed; -using Moq; using Xunit; namespace Open.IdentityServer.UnitTests.Services.Default; @@ -34,7 +35,20 @@ public class DistributedDeviceFlowThrottlingServiceTests private Mock _telemetry = new(); private Mock _trace = new(); - + private Mock _clientStore; + private Client _client; + + public DistributedDeviceFlowThrottlingServiceTests() + { + _clientStore = new Mock(); + _client = new Client + { + ClientId = "client" + }; + _clientStore.Setup(x => x.FindClientByIdAsync(It.IsAny())) + .ReturnsAsync(_client); + } + [Fact] public async Task First_Poll() { @@ -52,6 +66,7 @@ private DistributedDeviceFlowThrottlingService CreateSubject() { return new DistributedDeviceFlowThrottlingService( cache, + _clientStore.Object, new StubClock {UtcNowFunc = () => testDate}, options, _telemetry.Object); @@ -76,10 +91,11 @@ public async Task ShouldSlowDown_ShouldInitiateTelemetryTrace() } [Fact] - public async Task Second_Poll_Too_Fast() + public async Task Second_Poll_Too_Fast_For_Options() { var handle = Guid.NewGuid().ToString(); var service = CreateSubject(); + _client.PollingInterval = null; await cache.SetAsync( CacheKey + handle, @@ -94,11 +110,31 @@ await cache.SetAsync( } [Fact] - public async Task Second_Poll_After_Interval() + public async Task Second_Poll_Too_Fast_For_Client() + { + var handle = Guid.NewGuid().ToString(); + var service = CreateSubject(); + _client.PollingInterval = 4; + + await cache.SetAsync( + CacheKey + handle, + Encoding.UTF8.GetBytes(testDate.AddSeconds(-2).ToString("O")), + TestContext.Current.CancellationToken); + + var result = await service.ShouldSlowDown(handle, deviceCode); + + result.Should().BeTrue(); + + CheckCacheEntry(handle); + } + + [Fact] + public async Task Second_Poll_After_Options_Interval() { var handle = Guid.NewGuid().ToString(); var service = CreateSubject(); + _client.PollingInterval = null; await cache.SetAsync( $"devicecode_{handle}", @@ -112,6 +148,26 @@ await cache.SetAsync( CheckCacheEntry(handle); } + [Fact] + public async Task Second_Poll_After_Client_Interval() + { + var handle = Guid.NewGuid().ToString(); + + var service = CreateSubject(); + _client.PollingInterval = 4; + + await cache.SetAsync( + $"devicecode_{handle}", + Encoding.UTF8.GetBytes(testDate.AddSeconds(-deviceCode.Lifetime - 2).ToString("O")), + TestContext.Current.CancellationToken); + + var result = await service.ShouldSlowDown(handle, deviceCode); + + result.Should().BeFalse(); + + CheckCacheEntry(handle); + } + /// /// Addresses race condition from #3860 ///