Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public Models.Client ToModel()
DeviceCodeLifetime = clientEntity.DeviceCodeLifetime,
AllowedCorsOrigins = clientEntity.AllowedCorsOrigins.ToStringCollection(),
Properties = clientEntity.Properties.ToModelDictionary(),
PollingInterval = clientEntity.PollingInterval,
};
}
}
Expand Down Expand Up @@ -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<Entities.ClientProperty>(),
PollingInterval = clientModel.PollingInterval,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -28,16 +30,19 @@ public class DistributedDeviceFlowThrottlingService : IDeviceFlowThrottlingServi
/// Initializes a new instance of the <see cref="DistributedDeviceFlowThrottlingService"/> class.
/// </summary>
/// <param name="cache">The cache.</param>
/// <param name="clientStore">The client store.</param>
/// <param name="clock">The clock.</param>
/// <param name="options">The options.</param>
/// <param name="telemetry">The telemetry</param>
public DistributedDeviceFlowThrottlingService(
IDistributedCache cache,
IClientStore clientStore,
TimeProvider clock,
IdentityServerOptions options,
ITelemetryService telemetry)
{
_cache = cache;
_clientStore = clientStore;
_clock = clock;
_options = options;
_telemetry = telemetry;
Expand Down Expand Up @@ -70,7 +75,8 @@ public async Task<bool> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,7 +35,20 @@ public class DistributedDeviceFlowThrottlingServiceTests

private Mock<ITelemetryService> _telemetry = new();
private Mock<ITrace> _trace = new();

private Mock<IClientStore> _clientStore;
private Client _client;

public DistributedDeviceFlowThrottlingServiceTests()
{
_clientStore = new Mock<IClientStore>();
_client = new Client
{
ClientId = "client"
};
_clientStore.Setup(x => x.FindClientByIdAsync(It.IsAny<string>()))
.ReturnsAsync(_client);
}

[Fact]
public async Task First_Poll()
{
Expand All @@ -52,6 +66,7 @@ private DistributedDeviceFlowThrottlingService CreateSubject()
{
return new DistributedDeviceFlowThrottlingService(
cache,
_clientStore.Object,
new StubClock {UtcNowFunc = () => testDate},
options,
_telemetry.Object);
Expand All @@ -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,
Expand All @@ -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}",
Expand All @@ -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);
}

/// <summary>
/// Addresses race condition from #3860
/// </summary>
Expand Down
Loading