Skip to content

Commit acf38ca

Browse files
authored
Feature/dev 356 (#62)
* DEV-356 cache groups * DEV-356 ip white list
1 parent 4c3f960 commit acf38ca

6 files changed

Lines changed: 104 additions & 10 deletions

File tree

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,43 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24

35
namespace MultiFactor.Radius.Adapter.Configuration
46
{
57
public class AuthenticatedClientCacheConfig
68
{
79
public TimeSpan Lifetime { get; }
810
public bool Enabled => Lifetime != TimeSpan.Zero;
11+
public IReadOnlyCollection<string> AuthenticationCacheGroups { get; }
912

10-
public AuthenticatedClientCacheConfig(TimeSpan lifetime)
13+
public AuthenticatedClientCacheConfig(TimeSpan lifetime, IReadOnlyCollection<string> authenticationCacheGroups = null)
1114
{
1215
Lifetime = lifetime;
16+
AuthenticationCacheGroups = authenticationCacheGroups?.Select(x => x.ToLower()).ToArray() ?? Array.Empty<string>();
1317
}
1418

15-
public static AuthenticatedClientCacheConfig CreateFromTimeSpan(string value)
19+
public static AuthenticatedClientCacheConfig CreateFromTimeSpan(string value, string authenticationCacheGroups = null)
1620
{
1721
if (string.IsNullOrWhiteSpace(value)) return new AuthenticatedClientCacheConfig(TimeSpan.Zero);
18-
return new AuthenticatedClientCacheConfig(TimeSpan.ParseExact(value, @"hh\:mm\:ss", null, System.Globalization.TimeSpanStyles.None));
22+
var cacheGroups = SplitCacheGroup(authenticationCacheGroups);
23+
return new AuthenticatedClientCacheConfig(TimeSpan.ParseExact(value, @"hh\:mm\:ss", null, System.Globalization.TimeSpanStyles.None), cacheGroups);
1924
}
2025

21-
public static AuthenticatedClientCacheConfig CreateFromMinutes(string value)
26+
public static AuthenticatedClientCacheConfig CreateFromMinutes(string value, string authenticationCacheGroups = null)
2227
{
2328
if (string.IsNullOrWhiteSpace(value)) return new AuthenticatedClientCacheConfig(TimeSpan.Zero);
24-
return new AuthenticatedClientCacheConfig(TimeSpan.FromMinutes(int.Parse(value)));
29+
var cacheGroups = SplitCacheGroup(authenticationCacheGroups);
30+
return new AuthenticatedClientCacheConfig(TimeSpan.FromMinutes(int.Parse(value)), cacheGroups);
31+
}
32+
33+
private static string[] SplitCacheGroup(string cacheGroup)
34+
{
35+
return cacheGroup
36+
?.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries)
37+
.Select(x => x.ToLower().Trim())
38+
.Where(x => !string.IsNullOrWhiteSpace(x))
39+
.Distinct()
40+
.ToArray() ?? Array.Empty<string>();
2541
}
2642
}
2743
}

MultiFactor.Radius.Adapter/Configuration/ClientConfiguration.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Collections.Generic;
1010
using System.Linq;
1111
using System.Net;
12+
using NetTools;
1213

1314
namespace MultiFactor.Radius.Adapter.Configuration
1415
{
@@ -17,6 +18,8 @@ namespace MultiFactor.Radius.Adapter.Configuration
1718
/// </summary>
1819
public class ClientConfiguration
1920
{
21+
private readonly List<IPAddressRange> _ipAddressRanges = new List<IPAddressRange>();
22+
2023
public ClientConfiguration()
2124
{
2225
BypassSecondFactorWhenApiUnreachable = true; //by default
@@ -205,6 +208,7 @@ public bool ShouldLoadUserGroups()
205208
return
206209
ActiveDirectoryGroup.Any() ||
207210
ActiveDirectory2FaGroup.Any() ||
211+
AuthenticationCacheLifetime.AuthenticationCacheGroups.Any() ||
208212
RadiusReplyAttributes
209213
.Values
210214
.SelectMany(attr => attr)
@@ -233,5 +237,9 @@ public bool ShouldLoadUserGroups()
233237
/// Ldap connection timeout
234238
/// </summary>
235239
public TimeSpan LdapBindTimeout { get; set; } = new TimeSpan(0, 0, 30);
240+
241+
public IReadOnlyCollection<IPAddressRange> IpWhiteAddressRanges => _ipAddressRanges;
242+
243+
public void AddWhiteIpRange(IPAddressRange range) => _ipAddressRanges.Add(range);
236244
}
237245
}

MultiFactor.Radius.Adapter/Configuration/ServiceConfiguration.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,8 @@ public static ClientConfiguration LoadClientSettings(string name,
505505
configuration.LdapBindTimeout = ldapBindTimeout;
506506
}
507507
}
508+
509+
ReadIpWhiteList(configuration, appSettings.Settings["ip-white-list"]?.Value);
508510

509511
return configuration;
510512
}
@@ -513,15 +515,16 @@ private static void ReadAuthenticationCacheSetting(AppSettingsSection appSetting
513515
{
514516
var setting = appSettings.Settings[Constants.Configuration.AuthenticationCacheLifetime]?.Value;
515517
var legacySetting = appSettings.Settings[Constants.Configuration.BypassSecondFactorPeriod]?.Value;
518+
var groups = appSettings.Settings["authentication-cache-groups"]?.Value;
516519
try
517520
{
518521
if (setting != null)
519522
{
520-
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromTimeSpan(setting);
523+
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromTimeSpan(setting, groups);
521524
}
522525
else
523526
{
524-
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromMinutes(legacySetting);
527+
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromMinutes(legacySetting, groups);
525528
}
526529

527530
}
@@ -811,6 +814,23 @@ private static void ReadSignUpGroupsSettings(ClientConfiguration configuration,
811814

812815
configuration.SignUpGroups = signUpGroupsSettings;
813816
}
817+
818+
private static void ReadIpWhiteList(ClientConfiguration builder, string ipWhiteList)
819+
{
820+
var splittedRanges = ipWhiteList
821+
?.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries)
822+
.Select(x => x.ToLower().Trim())
823+
.Where(x => !string.IsNullOrWhiteSpace(x))
824+
.Distinct()
825+
.ToArray() ?? Array.Empty<string>();
826+
827+
foreach (var range in splittedRanges)
828+
{
829+
if (!IPAddressRange.TryParse(range, out var ipAddressRange))
830+
throw new Exception($"Invalid IP address range: '{range}' in '{builder.Name}' config");
831+
builder.AddWhiteIpRange(ipAddressRange);
832+
}
833+
}
814834

815835
#endregion
816836

MultiFactor.Radius.Adapter/Server/RadiusRouter.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System;
1313
using System.Collections.Concurrent;
1414
using System.Linq;
15+
using System.Net;
1516
using System.Text;
1617
using System.Text.RegularExpressions;
1718
using System.Threading.Tasks;
@@ -47,7 +48,21 @@ public async Task HandleRequest(PendingRequest request)
4748
{
4849
try
4950
{
50-
if (request.RequestPacket.Header.Code == PacketCode.StatusServer)
51+
var rangesStr = string.Join(", ", request.Configuration.IpWhiteAddressRanges);
52+
if (!IsAllowedClientIp(request))
53+
{
54+
_logger.Debug("Client '{clientIp}' is not in the allowed IP range: ({ranges})", request.RemoteEndpoint.Address, rangesStr);
55+
56+
request.AuthenticationState.Reject();
57+
58+
CreateAndSendRadiusResponse(request);
59+
return;
60+
}
61+
62+
if (!string.IsNullOrWhiteSpace(rangesStr))
63+
_logger.Debug("Client '{clientIp}' is in the allowed IP range: ({ranges})", request.RemoteEndpoint.Address, rangesStr);
64+
65+
if (request.RequestPacket.Header.Code == PacketCode.StatusServer)
5166
{
5267
//status
5368
var uptime = (DateTime.Now - _startedAt);
@@ -433,5 +448,21 @@ private void RemoveStateChallengeRequest(string state)
433448
{
434449
_stateChallengePendingRequests.TryRemove(state, out PendingRequest _);
435450
}
451+
452+
private bool IsAllowedClientIp(PendingRequest request)
453+
{
454+
var ipWhiteList = request.Configuration.IpWhiteAddressRanges;
455+
if (ipWhiteList.Count == 0)
456+
return true;
457+
458+
var callingStationId = request.RequestPacket.CallingStationId;
459+
460+
var clientIp = IPAddress.TryParse(callingStationId ?? string.Empty, out var callingStationIp)
461+
? callingStationIp
462+
: request.RemoteEndpoint.Address;
463+
464+
var isIpInRange = ipWhiteList.Any(x => x.Contains(clientIp));
465+
return isIpInRange;
466+
}
436467
}
437468
}

MultiFactor.Radius.Adapter/Services/AuthenticatedClientCache.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using Serilog;
33
using System;
44
using System.Collections.Concurrent;
5+
using System.Collections.Generic;
6+
using System.Linq;
57

68
namespace MultiFactor.Radius.Adapter.Services
79
{
@@ -15,9 +17,26 @@ public AuthenticatedClientCache(ILogger logger)
1517
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
1618
}
1719

18-
public bool TryHitCache(string callingStationId, string userName, ClientConfiguration clientConfiguration)
20+
public bool TryHitCache(string callingStationId, string userName, ClientConfiguration clientConfiguration, IReadOnlyCollection<string> userGroups)
1921
{
22+
if (userGroups is null)
23+
throw new ArgumentException(nameof(userGroups));
24+
2025
if (!clientConfiguration.AuthenticationCacheLifetime.Enabled) return false;
26+
27+
var cacheGroups = clientConfiguration.AuthenticationCacheLifetime.AuthenticationCacheGroups;
28+
var lowercaseUserGroups = userGroups.Select(x => x.ToLower().Trim());
29+
var groupsStr = string.Join(", ", cacheGroups);
30+
if (cacheGroups.Count > 0 && !cacheGroups.Intersect(lowercaseUserGroups).Any())
31+
{
32+
_logger.Debug("Skip auth caching. User '{userName}' is not a member of any authentication cache groups: ({groups})", userName, groupsStr);
33+
return false;
34+
}
35+
36+
if (!string.IsNullOrEmpty(groupsStr))
37+
{
38+
_logger.Debug("User '{userName}' is a member of authentication cache groups: ({groups})", userName, groupsStr);
39+
}
2140

2241
if (string.IsNullOrEmpty(callingStationId))
2342
{

MultiFactor.Radius.Adapter/Services/MultiFactorApi/MultifactorApiAdapter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public async Task<SecondFactorResponse> CreateSecondFactorRequestAsync(PendingRe
6262
}
6363

6464
//try to get authenticated client to bypass second factor if configured
65-
if (_authenticatedClientCache.TryHitCache(callingStationId, userName, request.Configuration))
65+
if (_authenticatedClientCache.TryHitCache(callingStationId, userName, request.Configuration, request.Profile.MemberOf))
6666
{
6767
_logger.Information("Bypass second factor for user '{name:l}' with identity '{user:l}' from {host:l}:{port}", request.UserName, userName, request.RemoteEndpoint.Address, request.RemoteEndpoint.Port);
6868
return new SecondFactorResponse(PacketCode.AccessAccept);

0 commit comments

Comments
 (0)