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
@@ -1,27 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace MultiFactor.Radius.Adapter.Configuration
{
public class AuthenticatedClientCacheConfig
{
public TimeSpan Lifetime { get; }
public bool Enabled => Lifetime != TimeSpan.Zero;
public IReadOnlyCollection<string> AuthenticationCacheGroups { get; }

public AuthenticatedClientCacheConfig(TimeSpan lifetime)
public AuthenticatedClientCacheConfig(TimeSpan lifetime, IReadOnlyCollection<string> authenticationCacheGroups = null)
{
Lifetime = lifetime;
AuthenticationCacheGroups = authenticationCacheGroups?.Select(x => x.ToLower()).ToArray() ?? Array.Empty<string>();
}

public static AuthenticatedClientCacheConfig CreateFromTimeSpan(string value)
public static AuthenticatedClientCacheConfig CreateFromTimeSpan(string value, string authenticationCacheGroups = null)
{
if (string.IsNullOrWhiteSpace(value)) return new AuthenticatedClientCacheConfig(TimeSpan.Zero);
return new AuthenticatedClientCacheConfig(TimeSpan.ParseExact(value, @"hh\:mm\:ss", null, System.Globalization.TimeSpanStyles.None));
var cacheGroups = SplitCacheGroup(authenticationCacheGroups);
return new AuthenticatedClientCacheConfig(TimeSpan.ParseExact(value, @"hh\:mm\:ss", null, System.Globalization.TimeSpanStyles.None), cacheGroups);
}

public static AuthenticatedClientCacheConfig CreateFromMinutes(string value)
public static AuthenticatedClientCacheConfig CreateFromMinutes(string value, string authenticationCacheGroups = null)
{
if (string.IsNullOrWhiteSpace(value)) return new AuthenticatedClientCacheConfig(TimeSpan.Zero);
return new AuthenticatedClientCacheConfig(TimeSpan.FromMinutes(int.Parse(value)));
var cacheGroups = SplitCacheGroup(authenticationCacheGroups);
return new AuthenticatedClientCacheConfig(TimeSpan.FromMinutes(int.Parse(value)), cacheGroups);
}

private static string[] SplitCacheGroup(string cacheGroup)
{
return cacheGroup
?.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.ToLower().Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToArray() ?? Array.Empty<string>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NetTools;

namespace MultiFactor.Radius.Adapter.Configuration
{
Expand All @@ -17,6 +18,8 @@ namespace MultiFactor.Radius.Adapter.Configuration
/// </summary>
public class ClientConfiguration
{
private readonly List<IPAddressRange> _ipAddressRanges = new List<IPAddressRange>();

public ClientConfiguration()
{
BypassSecondFactorWhenApiUnreachable = true; //by default
Expand Down Expand Up @@ -205,6 +208,7 @@ public bool ShouldLoadUserGroups()
return
ActiveDirectoryGroup.Any() ||
ActiveDirectory2FaGroup.Any() ||
AuthenticationCacheLifetime.AuthenticationCacheGroups.Any() ||
RadiusReplyAttributes
.Values
.SelectMany(attr => attr)
Expand Down Expand Up @@ -233,5 +237,9 @@ public bool ShouldLoadUserGroups()
/// Ldap connection timeout
/// </summary>
public TimeSpan LdapBindTimeout { get; set; } = new TimeSpan(0, 0, 30);

public IReadOnlyCollection<IPAddressRange> IpWhiteAddressRanges => _ipAddressRanges;

public void AddWhiteIpRange(IPAddressRange range) => _ipAddressRanges.Add(range);
}
}
24 changes: 22 additions & 2 deletions MultiFactor.Radius.Adapter/Configuration/ServiceConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,8 @@ public static ClientConfiguration LoadClientSettings(string name,
configuration.LdapBindTimeout = ldapBindTimeout;
}
}

ReadIpWhiteList(configuration, appSettings.Settings["ip-white-list"]?.Value);

return configuration;
}
Expand All @@ -513,15 +515,16 @@ private static void ReadAuthenticationCacheSetting(AppSettingsSection appSetting
{
var setting = appSettings.Settings[Constants.Configuration.AuthenticationCacheLifetime]?.Value;
var legacySetting = appSettings.Settings[Constants.Configuration.BypassSecondFactorPeriod]?.Value;
var groups = appSettings.Settings["authentication-cache-groups"]?.Value;
try
{
if (setting != null)
{
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromTimeSpan(setting);
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromTimeSpan(setting, groups);
}
else
{
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromMinutes(legacySetting);
configuration.AuthenticationCacheLifetime = AuthenticatedClientCacheConfig.CreateFromMinutes(legacySetting, groups);
}

}
Expand Down Expand Up @@ -811,6 +814,23 @@ private static void ReadSignUpGroupsSettings(ClientConfiguration configuration,

configuration.SignUpGroups = signUpGroupsSettings;
}

private static void ReadIpWhiteList(ClientConfiguration builder, string ipWhiteList)
{
var splittedRanges = ipWhiteList
?.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.ToLower().Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToArray() ?? Array.Empty<string>();

foreach (var range in splittedRanges)
{
if (!IPAddressRange.TryParse(range, out var ipAddressRange))
throw new Exception($"Invalid IP address range: '{range}' in '{builder.Name}' config");
builder.AddWhiteIpRange(ipAddressRange);
}
}

#endregion

Expand Down
33 changes: 32 additions & 1 deletion MultiFactor.Radius.Adapter/Server/RadiusRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
Expand Down Expand Up @@ -47,7 +48,21 @@ public async Task HandleRequest(PendingRequest request)
{
try
{
if (request.RequestPacket.Header.Code == PacketCode.StatusServer)
var rangesStr = string.Join(", ", request.Configuration.IpWhiteAddressRanges);
if (!IsAllowedClientIp(request))
{
_logger.Debug("Client '{clientIp}' is not in the allowed IP range: ({ranges})", request.RemoteEndpoint.Address, rangesStr);

request.AuthenticationState.Reject();

CreateAndSendRadiusResponse(request);
return;
}

if (!string.IsNullOrWhiteSpace(rangesStr))
_logger.Debug("Client '{clientIp}' is in the allowed IP range: ({ranges})", request.RemoteEndpoint.Address, rangesStr);

if (request.RequestPacket.Header.Code == PacketCode.StatusServer)
{
//status
var uptime = (DateTime.Now - _startedAt);
Expand Down Expand Up @@ -433,5 +448,21 @@ private void RemoveStateChallengeRequest(string state)
{
_stateChallengePendingRequests.TryRemove(state, out PendingRequest _);
}

private bool IsAllowedClientIp(PendingRequest request)
{
var ipWhiteList = request.Configuration.IpWhiteAddressRanges;
if (ipWhiteList.Count == 0)
return true;

var callingStationId = request.RequestPacket.CallingStationId;

var clientIp = IPAddress.TryParse(callingStationId ?? string.Empty, out var callingStationIp)
? callingStationIp
: request.RemoteEndpoint.Address;

var isIpInRange = ipWhiteList.Any(x => x.Contains(clientIp));
return isIpInRange;
}
}
}
21 changes: 20 additions & 1 deletion MultiFactor.Radius.Adapter/Services/AuthenticatedClientCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Serilog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

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

public bool TryHitCache(string callingStationId, string userName, ClientConfiguration clientConfiguration)
public bool TryHitCache(string callingStationId, string userName, ClientConfiguration clientConfiguration, IReadOnlyCollection<string> userGroups)
{
if (userGroups is null)
throw new ArgumentException(nameof(userGroups));

if (!clientConfiguration.AuthenticationCacheLifetime.Enabled) return false;

var cacheGroups = clientConfiguration.AuthenticationCacheLifetime.AuthenticationCacheGroups;
var lowercaseUserGroups = userGroups.Select(x => x.ToLower().Trim());
var groupsStr = string.Join(", ", cacheGroups);
if (cacheGroups.Count > 0 && !cacheGroups.Intersect(lowercaseUserGroups).Any())
{
_logger.Debug("Skip auth caching. User '{userName}' is not a member of any authentication cache groups: ({groups})", userName, groupsStr);
return false;
}

if (!string.IsNullOrEmpty(groupsStr))
{
_logger.Debug("User '{userName}' is a member of authentication cache groups: ({groups})", userName, groupsStr);
}

if (string.IsNullOrEmpty(callingStationId))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public async Task<SecondFactorResponse> CreateSecondFactorRequestAsync(PendingRe
}

//try to get authenticated client to bypass second factor if configured
if (_authenticatedClientCache.TryHitCache(callingStationId, userName, request.Configuration))
if (_authenticatedClientCache.TryHitCache(callingStationId, userName, request.Configuration, request.Profile.MemberOf))
{
_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);
return new SecondFactorResponse(PacketCode.AccessAccept);
Expand Down