Skip to content

Commit

Permalink
Remote Desktop Gateway Active Directory group membership checks
Browse files Browse the repository at this point in the history
  • Loading branch information
KonstantinYan committed Nov 25, 2020
1 parent 0606f09 commit 5fa03e9
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 75 deletions.
30 changes: 27 additions & 3 deletions MultiFactor.Radius.Adapter/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public Configuration()
BypassSecondFactorWhenApiUnreachable = true; //by default
}

#region general settings

/// <summary>
/// This service RADIUS UDP Server endpoint
Expand All @@ -47,6 +48,8 @@ public Configuration()
/// </summary>
public bool BypassSecondFactorWhenApiUnreachable { get; set; }

#endregion

#region ActiveDirectory Authentication settings

/// <summary>
Expand Down Expand Up @@ -74,6 +77,21 @@ public Configuration()
/// </summary>
public bool UseActiveDirectoryMobileUserPhone { get; set; }

/// <summary>
/// Load user profile from AD and check group membership and
/// </summary>
public bool CheckMembership
{
get
{
return ActiveDirectoryDomain != null &&
(ActiveDirectoryGroup != null ||
ActiveDirectory2FaGroup != null ||
UseActiveDirectoryUserPhone ||
UseActiveDirectoryMobileUserPhone);
}
}

#endregion

#region RADIUS Authentication settings
Expand Down Expand Up @@ -207,19 +225,25 @@ public static Configuration Load(IRadiusDictionary dictionary)
switch (configuration.FirstFactorAuthenticationSource)
{
case AuthenticationSource.ActiveDirectory:
LoadActiveDirectoryAuthenticationSourceSettings(configuration);
//active directory authentication and membership settings
LoadActiveDirectoryAuthenticationSourceSettings(configuration, true);
break;
case AuthenticationSource.Radius:
//radius authentication settings
LoadRadiusAuthenticationSourceSettings(configuration);
break;
case AuthenticationSource.None:
//active directory membership only settings
LoadActiveDirectoryAuthenticationSourceSettings(configuration, false);
break;
}

LoadRadiusReplyAttributes(configuration, dictionary);

return configuration;
}

private static void LoadActiveDirectoryAuthenticationSourceSettings(Configuration configuration)
private static void LoadActiveDirectoryAuthenticationSourceSettings(Configuration configuration, bool mandatory)
{
var appSettings = ConfigurationManager.AppSettings;

Expand All @@ -229,7 +253,7 @@ private static void LoadActiveDirectoryAuthenticationSourceSettings(Configuratio
var useActiveDirectoryUserPhoneSetting = appSettings["use-active-directory-user-phone"];
var useActiveDirectoryMobileUserPhoneSetting = appSettings["use-active-directory-mobile-user-phone"];

if (string.IsNullOrEmpty(activeDirectoryDomainSetting))
if (mandatory && string.IsNullOrEmpty(activeDirectoryDomainSetting))
{
throw new Exception("Configuration error: 'active-directory-domain' element not found");
}
Expand Down
24 changes: 23 additions & 1 deletion MultiFactor.Radius.Adapter/Server/RadiusRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ private PacketCode ProcessFirstAuthenticationFactor(PendingRequest request)
case AuthenticationSource.Radius: //RADIUS auth
return ProcessRadiusAuthentication(request);
case AuthenticationSource.None:
if (_configuration.CheckMembership) //check membership without authentication
{
return ProcessActiveDirectoryMembership(request);
}
return PacketCode.AccessAccept; //first factor not required
default: //unknown source
throw new NotImplementedException(_configuration.FirstFactorAuthenticationSource.ToString());
Expand Down Expand Up @@ -140,7 +144,25 @@ private PacketCode ProcessActiveDirectoryAuthentication(PendingRequest request)
return PacketCode.AccessReject;
}

var isValid = _activeDirectoryService.VerifyCredential(userName, password, request);
var isValid = _activeDirectoryService.VerifyCredentialAndMembership(userName, password, request);

return isValid ? PacketCode.AccessAccept : PacketCode.AccessReject;
}

/// <summary>
/// Validate user membership within Active Directory Domain withoout password authentication
/// </summary>
private PacketCode ProcessActiveDirectoryMembership(PendingRequest request)
{
var userName = request.RequestPacket.UserName;

if (string.IsNullOrEmpty(userName))
{
_logger.Warning($"Can't find User-Name in message Id={request.RequestPacket.Identifier} from {request.RemoteEndpoint}");
return PacketCode.AccessReject;
}

var isValid = _activeDirectoryService.VerifyMembership(userName, request);

return isValid ? PacketCode.AccessAccept : PacketCode.AccessReject;
}
Expand Down
176 changes: 105 additions & 71 deletions MultiFactor.Radius.Adapter/Services/ActiveDirectoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public ActiveDirectoryService(Configuration configuration, ILogger logger)
/// <summary>
/// Verify User Name, Password, User Status and Policy against Active Directory
/// </summary>
public bool VerifyCredential(string userName, string password, PendingRequest request)
public bool VerifyCredentialAndMembership(string userName, string password, PendingRequest request)
{
if (string.IsNullOrEmpty(userName))
{
Expand All @@ -60,100 +60,134 @@ public bool VerifyCredential(string userName, string password, PendingRequest re
connection.Bind();

_logger.Information($"User '{user.Name}' credential and status verified successfully at {_configuration.ActiveDirectoryDomain}");

var domain = LdapIdentity.FqdnToDn(_configuration.ActiveDirectoryDomain);

LoadForestSchema(connection, domain);

var isProfileLoaded = LoadProfile(connection, domain, user, out var profile);
if (!isProfileLoaded)

return VerifyMembership(connection, user, request);
}
}
catch (LdapException lex)
{
if (lex.ServerErrorMessage != null)
{
var dataReason = ExtractErrorReason(lex.ServerErrorMessage);
if (dataReason != null)
{
_logger.Warning($"Verification user '{user.Name}' at {_configuration.ActiveDirectoryDomain} failed: {dataReason}");
return false;
}
}

var checkGroupMembership = !string.IsNullOrEmpty(_configuration.ActiveDirectoryGroup);
//user must be member of security group
if (checkGroupMembership)
{
var isMemberOf = IsMemberOf(connection, profile.BaseDn, user, _configuration.ActiveDirectoryGroup);
_logger.Error(lex, $"Verification user '{user.Name}' at {_configuration.ActiveDirectoryDomain} failed");
}
catch (Exception ex)
{
_logger.Error(ex, $"Verification user '{user.Name}' at {_configuration.ActiveDirectoryDomain} failed");
}

if (!isMemberOf)
{
_logger.Warning($"User '{user.Name}' is not member of '{_configuration.ActiveDirectoryGroup}' group in {profile.BaseDn.Name}");
return false;
}
return false;
}

_logger.Debug($"User '{user.Name}' is member of '{_configuration.ActiveDirectoryGroup}' group in {profile.BaseDn.Name}");
}
public bool VerifyMembership(string userName, PendingRequest request)
{
if (string.IsNullOrEmpty(userName))
{
throw new ArgumentNullException(nameof(userName));
}

var onlyMembersOfGroupMustProcess2faAuthentication = !string.IsNullOrEmpty(_configuration.ActiveDirectory2FaGroup);
//only users from group must process 2fa
if (onlyMembersOfGroupMustProcess2faAuthentication)
{
var isMemberOf = IsMemberOf(connection, profile.BaseDn, user, _configuration.ActiveDirectory2FaGroup);
var user = LdapIdentity.ParseUser(userName);

if (isMemberOf)
{
_logger.Debug($"User '{user.Name}' is member of '{_configuration.ActiveDirectory2FaGroup}' in {profile.BaseDn.Name}");
}
else
{
_logger.Information($"User '{user.Name}' is not member of '{_configuration.ActiveDirectory2FaGroup}' in {profile.BaseDn.Name}");
request.Bypass2Fa = true;
}
}
try
{
_logger.Debug($"Verifying user '{user.Name}' membership at {_configuration.ActiveDirectoryDomain}");

//check groups membership for radius reply conditional attributes
foreach (var attribute in _configuration.RadiusReplyAttributes)
{
foreach (var value in attribute.Value.Where(val => val.UserGroupCondition != null))
{
if (IsMemberOf(connection, profile.BaseDn, user, value.UserGroupCondition))
{
_logger.Information($"User '{user.Name}' is member of '{value.UserGroupCondition}' in {profile.BaseDn.Name}. Adding attribute '{attribute.Key}:{value.Value}' to reply");
request.UserGroups.Add(value.UserGroupCondition);
}
else
{
_logger.Debug($"User '{user.Name}' is not member of '{value.UserGroupCondition}' in {profile.BaseDn.Name}");
}
}
}
using (var connection = new LdapConnection(_configuration.ActiveDirectoryDomain))
{
connection.SessionOptions.RootDseCache = true;
connection.Bind();

if (_configuration.UseActiveDirectoryUserPhone)
{
request.UserPhone = profile.Phone;
}
if (_configuration.UseActiveDirectoryMobileUserPhone)
{
request.UserPhone = profile.Mobile;
}
request.EmailAddress = profile.Email;
return VerifyMembership(connection, user, request);
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Verification user '{user.Name}' membership at {_configuration.ActiveDirectoryDomain} failed");
_logger.Information("Run MultiFactor.Raduis.Adapter as user with domain read permissions (basically any domain user)");
}

return false;
}

private bool VerifyMembership(LdapConnection connection, LdapIdentity user, PendingRequest request)
{
var domain = LdapIdentity.FqdnToDn(_configuration.ActiveDirectoryDomain);

LoadForestSchema(connection, domain);

return true; //OK
var isProfileLoaded = LoadProfile(connection, domain, user, out var profile);
if (!isProfileLoaded)
{
return false;
}
catch (LdapException lex)

var checkGroupMembership = !string.IsNullOrEmpty(_configuration.ActiveDirectoryGroup);
//user must be member of security group
if (checkGroupMembership)
{
if (lex.ServerErrorMessage != null)
var isMemberOf = IsMemberOf(connection, profile.BaseDn, user, _configuration.ActiveDirectoryGroup);

if (!isMemberOf)
{
var dataReason = ExtractErrorReason(lex.ServerErrorMessage);
if (dataReason != null)
_logger.Warning($"User '{user.Name}' is not member of '{_configuration.ActiveDirectoryGroup}' group in {profile.BaseDn.Name}");
return false;
}

_logger.Debug($"User '{user.Name}' is member of '{_configuration.ActiveDirectoryGroup}' group in {profile.BaseDn.Name}");
}

var onlyMembersOfGroupMustProcess2faAuthentication = !string.IsNullOrEmpty(_configuration.ActiveDirectory2FaGroup);
//only users from group must process 2fa
if (onlyMembersOfGroupMustProcess2faAuthentication)
{
var isMemberOf = IsMemberOf(connection, profile.BaseDn, user, _configuration.ActiveDirectory2FaGroup);

if (isMemberOf)
{
_logger.Debug($"User '{user.Name}' is member of '{_configuration.ActiveDirectory2FaGroup}' in {profile.BaseDn.Name}");
}
else
{
_logger.Information($"User '{user.Name}' is not member of '{_configuration.ActiveDirectory2FaGroup}' in {profile.BaseDn.Name}");
request.Bypass2Fa = true;
}
}

//check groups membership for radius reply conditional attributes
foreach (var attribute in _configuration.RadiusReplyAttributes)
{
foreach (var value in attribute.Value.Where(val => val.UserGroupCondition != null))
{
if (IsMemberOf(connection, profile.BaseDn, user, value.UserGroupCondition))
{
_logger.Warning($"Verification user '{user.Name}' at {_configuration.ActiveDirectoryDomain} failed: {dataReason}");
return false;
_logger.Information($"User '{user.Name}' is member of '{value.UserGroupCondition}' in {profile.BaseDn.Name}. Adding attribute '{attribute.Key}:{value.Value}' to reply");
request.UserGroups.Add(value.UserGroupCondition);
}
else
{
_logger.Debug($"User '{user.Name}' is not member of '{value.UserGroupCondition}' in {profile.BaseDn.Name}");
}
}
}

_logger.Error(lex, $"Verification user '{user.Name}' at {_configuration.ActiveDirectoryDomain} failed");
if (_configuration.UseActiveDirectoryUserPhone)
{
request.UserPhone = profile.Phone;
}
catch (Exception ex)
if (_configuration.UseActiveDirectoryMobileUserPhone)
{
_logger.Error(ex, $"Verification user '{user.Name}' at {_configuration.ActiveDirectoryDomain} failed");
request.UserPhone = profile.Mobile;
}
request.EmailAddress = profile.Email;

return false;
return true;
}

private void LoadForestSchema(LdapConnection connection, LdapIdentity root)
Expand Down

0 comments on commit 5fa03e9

Please sign in to comment.