diff --git a/Extensions/HttpContextBaseExtensions.cs b/Extensions/HttpContextBaseExtensions.cs
index a3c614f..0e54bed 100644
--- a/Extensions/HttpContextBaseExtensions.cs
+++ b/Extensions/HttpContextBaseExtensions.cs
@@ -16,7 +16,7 @@ public static CacheAdapter GetCacheAdapter(this HttpContextBase httpContext)
return new CacheAdapter(httpContext);
}
- public static bool HasApiUnreachableFlag(this HttpContextBase httpContext)
+ public static bool HasApiUnreachableFlag(this HttpContextBase httpContext, bool trimName = true)
{
var name = httpContext?.User?.Identity?.Name;
if (string.IsNullOrEmpty(name))
@@ -24,7 +24,12 @@ public static bool HasApiUnreachableFlag(this HttpContextBase httpContext)
return false;
}
- return httpContext.GetCacheAdapter().GetApiUnreachable(Util.CanonicalizeUserName(name));
+ // for owa we dont want trim username, because owa send netbiosname
+ // but for compatibility with Ms365 we use flag
+ if (trimName)
+ return httpContext.GetCacheAdapter().GetApiUnreachable(Util.CanonicalizeUserName(name));
+ else
+ return httpContext.GetCacheAdapter().GetApiUnreachable(name);
}
}
}
\ No newline at end of file
diff --git a/Interop/NameTranslator.cs b/Interop/NameTranslator.cs
new file mode 100644
index 0000000..1d74e17
--- /dev/null
+++ b/Interop/NameTranslator.cs
@@ -0,0 +1,60 @@
+using MultiFactor.IIS.Adapter.Services;
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using static MultiFactor.IIS.Adapter.Interop.NativeMethods;
+
+namespace MultiFactor.IIS.Adapter.Interop
+{
+ public class NameTranslator : IDisposable
+ {
+ private readonly SafeDsHandle _handle;
+ private readonly Logger _logger;
+ private readonly string _domain;
+
+ public NameTranslator(string domain, Logger logger)
+ {
+ _domain = domain;
+ _logger = logger;
+ uint res = DsBind(domain, null, out _handle);
+ if (res != (uint)DS_NAME_ERROR.DS_NAME_NO_ERROR)
+ {
+ _logger.Warn($"Failed to bind to: {domain}");
+ throw new Win32Exception((int)res);
+ }
+ }
+
+ public UserSearchContext Translate(string netbiosName)
+ {
+ uint err = DsCrackNames(_handle, DS_NAME_FLAGS.DS_NAME_FLAG_EVAL_AT_DC | DS_NAME_FLAGS.DS_NAME_FLAG_TRUST_REFERRAL, DS_NAME_FORMAT.DS_NT4_ACCOUNT_NAME, DS_NAME_FORMAT.DS_USER_PRINCIPAL_NAME, 1, new[] { netbiosName }, out IntPtr pResult);
+ if (err != (uint)DS_NAME_ERROR.DS_NAME_NO_ERROR)
+ {
+ _logger.Warn($"Failed to translate {netbiosName} in {_domain}");
+ throw new Win32Exception((int)err);
+ }
+
+ try
+ {
+ // Next convert the returned structure to managed environment
+ DS_NAME_RESULT Result = (DS_NAME_RESULT)Marshal.PtrToStructure(pResult, typeof(DS_NAME_RESULT));
+ var res = Result.Items;
+ if (res == null || res.Length == 0 || (!res[0].status.HasFlag(DS_NAME_ERROR.DS_NAME_ERROR_TRUST_REFERRAL) && !res[0].status.HasFlag(DS_NAME_ERROR.DS_NAME_NO_ERROR)))
+ {
+ _logger.Warn($"Unexpected result of translation {netbiosName} in {_domain}");
+ throw new System.Security.SecurityException("Unable to resolve user name.");
+ }
+ return new UserSearchContext(res[0].pDomain, res[0].pName, netbiosName);
+ }
+ finally
+ {
+ DsFreeNameResult(pResult);
+ }
+
+ }
+
+ public void Dispose()
+ {
+ _handle.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Interop/NativeMethods.cs b/Interop/NativeMethods.cs
new file mode 100644
index 0000000..29b6b83
--- /dev/null
+++ b/Interop/NativeMethods.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace MultiFactor.IIS.Adapter.Interop
+{
+ internal static class NativeMethods
+ {
+ private const string NTDSAPI = "ntdsapi.dll";
+
+ [DllImport(NTDSAPI, CharSet = CharSet.Auto)]
+ public static extern uint DsBind(
+ string DomainControllerName, // in, optional
+ string DnsDomainName, // in, optional
+ out SafeDsHandle phDS);
+
+ [DllImport(NTDSAPI, CharSet = CharSet.Auto)]
+ public static extern uint DsCrackNames(
+ SafeDsHandle hDS,
+ DS_NAME_FLAGS flags,
+ DS_NAME_FORMAT formatOffered,
+ DS_NAME_FORMAT formatDesired,
+ uint cNames,
+ [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPTStr, SizeParamIndex = 4)]
+ string[] rpNames,
+ out IntPtr ppResult);
+
+ [DllImport(NTDSAPI, CharSet = CharSet.Auto)]
+ public static extern void DsFreeNameResult(IntPtr pResult /* DS_NAME_RESULT* */);
+
+ [DllImport(NTDSAPI, CharSet = CharSet.Auto)]
+ public static extern uint DsUnBind(ref IntPtr phDS);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ public struct DS_NAME_RESULT
+ {
+ public uint cItems;
+ internal IntPtr rItems; // PDS_NAME_RESULT_ITEM
+ public DS_NAME_RESULT_ITEM[] Items
+ {
+ get
+ {
+ if (rItems == IntPtr.Zero)
+ {
+ return new DS_NAME_RESULT_ITEM[0];
+ }
+
+ var ResultArray = new DS_NAME_RESULT_ITEM[cItems];
+ Type strType = typeof(DS_NAME_RESULT_ITEM);
+ int stSize = Marshal.SizeOf(strType);
+ IntPtr curptr;
+
+ for (uint i = 0; i < cItems; i++)
+ {
+ curptr = new IntPtr(rItems.ToInt64() + (i * stSize));
+ ResultArray[i] = (DS_NAME_RESULT_ITEM)Marshal.PtrToStructure(curptr, strType);
+ }
+ return ResultArray;
+ }
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ public struct DS_NAME_RESULT_ITEM
+ {
+ public DS_NAME_ERROR status;
+ public string pDomain;
+ public string pName;
+ public override string ToString()
+ {
+ if (status == DS_NAME_ERROR.DS_NAME_NO_ERROR)
+ {
+ return pName;
+ }
+
+ return null;
+ }
+ }
+
+ public enum DS_NAME_ERROR
+ {
+ DS_NAME_NO_ERROR = 0,
+ // Generic processing error.
+ DS_NAME_ERROR_RESOLVING = 1,
+ // Couldn't find the name at all - or perhaps caller doesn't have
+ // rights to see it.
+ DS_NAME_ERROR_NOT_FOUND = 2,
+ // Input name mapped to more than one output name.
+ DS_NAME_ERROR_NOT_UNIQUE = 3,
+ // Input name found, but not the associated output format.
+ // Can happen if object doesn't have all the required attributes.
+ DS_NAME_ERROR_NO_MAPPING = 4,
+ // Unable to resolve entire name, but was able to determine which
+ // domain object resides in. Thus DS_NAME_RESULT_ITEM?.pDomain
+ // is valid on return.
+ DS_NAME_ERROR_DOMAIN_ONLY = 5,
+ // Unable to perform a purely syntactical mapping at the client
+ // without going out on the wire.
+ DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING = 6,
+ // The name is from an external trusted forest.
+ DS_NAME_ERROR_TRUST_REFERRAL = 7
+ }
+
+ [Flags]
+ public enum DS_NAME_FLAGS
+ {
+ DS_NAME_NO_FLAGS = 0x0,
+ // Perform a syntactical mapping at the client (if possible) without
+ // going out on the wire. Returns DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING
+ // if a purely syntactical mapping is not possible.
+ DS_NAME_FLAG_SYNTACTICAL_ONLY = 0x1,
+ // Force a trip to the DC for evaluation, even if this could be
+ // locally cracked syntactically.
+ DS_NAME_FLAG_EVAL_AT_DC = 0x2,
+ // The call fails if the DC is not a GC
+ DS_NAME_FLAG_GCVERIFY = 0x4,
+ // Enable cross forest trust referral
+ DS_NAME_FLAG_TRUST_REFERRAL = 0x8
+ }
+
+ public enum DS_NAME_FORMAT
+ {
+ // unknown name type
+ DS_UNKNOWN_NAME = 0,
+ // eg: CN=User Name,OU=Users,DC=Example,DC=Microsoft,DC=Com
+ DS_FQDN_1779_NAME = 1,
+ // eg: Example\UserN
+ // Domain-only version includes trailing '\\'.
+ DS_NT4_ACCOUNT_NAME = 2,
+ // Probably "User Name" but could be something else. I.e. The
+ // display name is not necessarily the defining RDN.
+ DS_DISPLAY_NAME = 3,
+ // obsolete - see #define later
+ // DS_DOMAIN_SIMPLE_NAME = 4,
+ // obsolete - see #define later
+ // DS_ENTERPRISE_SIMPLE_NAME = 5,
+ // String-ized GUID as returned by IIDFromString().
+ // eg: {4fa050f0-f561-11cf-bdd9-00aa003a77b6}
+ DS_UNIQUE_ID_NAME = 6,
+ // eg: example.microsoft.com/software/user name
+ // Domain-only version includes trailing '/'.
+ DS_CANONICAL_NAME = 7,
+ // eg: usern@example.microsoft.com
+ DS_USER_PRINCIPAL_NAME = 8,
+ // Same as DS_CANONICAL_NAME except that rightmost '/' is
+ // replaced with '\n' - even in domain-only case.
+ // eg: example.microsoft.com/software\nuser name
+ DS_CANONICAL_NAME_EX = 9,
+ // eg: www/www.microsoft.com@example.com - generalized service principal
+ // names.
+ DS_SERVICE_PRINCIPAL_NAME = 10,
+ // This is the string representation of a SID. Invalid for formatDesired.
+ // See sddl.h for SID binary <--> text conversion routines.
+ // eg: S-1-5-21-397955417-626881126-188441444-501
+ DS_SID_OR_SID_HISTORY_NAME = 11,
+ // Pseudo-name format so GetUserNameEx can return the DNS domain name to
+ // a caller. This level is not supported by the DS APIs.
+ DS_DNS_DOMAIN_NAME = 12
+ }
+ }
+}
\ No newline at end of file
diff --git a/Interop/SafeDsHandle.cs b/Interop/SafeDsHandle.cs
new file mode 100644
index 0000000..2ddc245
--- /dev/null
+++ b/Interop/SafeDsHandle.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+
+namespace MultiFactor.IIS.Adapter.Interop
+{
+ public class SafeDsHandle : SafeHandle
+ {
+ public SafeDsHandle() : base(IntPtr.Zero, true) { }
+
+ public override bool IsInvalid
+ {
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ [PrePrepareMethod]
+ get { return (handle == IntPtr.Zero); }
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ [PrePrepareMethod]
+
+ protected override bool ReleaseHandle()
+ {
+ uint ret = NativeMethods.DsUnBind(ref handle);
+ System.Diagnostics.Debug.WriteLineIf(ret != 0, "Error unbinding :\t" + ret.ToString());
+ return ret != 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Interop/UserSearchContext.cs b/Interop/UserSearchContext.cs
new file mode 100644
index 0000000..f6d212f
--- /dev/null
+++ b/Interop/UserSearchContext.cs
@@ -0,0 +1,23 @@
+using MultiFactor.IIS.Adapter.Services.Ldap;
+using System;
+
+namespace MultiFactor.IIS.Adapter.Interop
+{
+ public class UserSearchContext
+ {
+ public string Domain { get; set; }
+ public LdapIdentity UserIdentity { get; set; }
+
+ public UserSearchContext(string domain, string upn, string rawUserName)
+ {
+ if (string.IsNullOrWhiteSpace(domain))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(domain));
+ if (string.IsNullOrWhiteSpace(upn))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(upn));
+ Domain = domain;
+ UserIdentity = LdapIdentity.Parse(upn).WithRawName(rawUserName);
+ }
+
+ public override string ToString() => $"User:{UserIdentity.RawName}, UPN:{UserIdentity.Name}, Domain:{Domain}";
+ }
+}
\ No newline at end of file
diff --git a/MsDynamics365/Module.cs b/MsDynamics365/Module.cs
index eb1ad9f..ea822eb 100644
--- a/MsDynamics365/Module.cs
+++ b/MsDynamics365/Module.cs
@@ -2,6 +2,7 @@
using MultiFactor.IIS.Adapter.Extensions;
using MultiFactor.IIS.Adapter.Owa;
using MultiFactor.IIS.Adapter.Services;
+using MultiFactor.IIS.Adapter.Services.Ldap;
using System;
using System.Web;
@@ -49,9 +50,8 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
//not yet authenticated with login/pwd
return;
}
- var user = context.User.Identity.Name;
+ var user = LdapIdentity.Parse(context.User.Identity.Name);
- var canonicalUserName = Util.CanonicalizeUserName(user);
//process request or postback to/from MultiFactor
if (path.Contains(Constants.MULTIFACTOR_PAGE))
@@ -65,8 +65,8 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
}
var ad = new ActiveDirectoryService(context.GetCacheAdapter(), Logger.IIS);
- var secondFactorRequired = new UserRequiredSecondFactor(ad);
- if (!secondFactorRequired.Execute(canonicalUserName))
+ var secondFactorRequired = new UserRequiredSecondFactor(ad, Logger.IIS);
+ if (!secondFactorRequired.Execute(user))
{
//bypass 2fa
return;
@@ -74,8 +74,8 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
//mfa
var valSrv = new TokenValidationService(Logger.IIS);
- var checker = new AuthChecker(context, valSrv);
- var isAuthenticatedByMultifactor = checker.IsAuthenticated(user);
+ var checker = new AuthChecker(context, valSrv, Logger.IIS);
+ var isAuthenticatedByMultifactor = checker.IsAuthenticated(user.RawName);
if (isAuthenticatedByMultifactor || context.HasApiUnreachableFlag())
{
return;
diff --git a/MultiFactor.IIS.Adapter.csproj b/MultiFactor.IIS.Adapter.csproj
index 86d1f5d..6660327 100644
--- a/MultiFactor.IIS.Adapter.csproj
+++ b/MultiFactor.IIS.Adapter.csproj
@@ -69,6 +69,10 @@
+
+
+
+
@@ -77,6 +81,8 @@
True
Resources.resx
+
+
diff --git a/Owa/Module.cs b/Owa/Module.cs
index 875975a..77fd063 100644
--- a/Owa/Module.cs
+++ b/Owa/Module.cs
@@ -2,6 +2,7 @@
using MultiFactor.IIS.Adapter.Extensions;
using MultiFactor.IIS.Adapter.Properties;
using MultiFactor.IIS.Adapter.Services;
+using MultiFactor.IIS.Adapter.Services.Ldap;
using System;
using System.Linq;
using System.Security.Principal;
@@ -61,14 +62,13 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
return;
}
- var user = context.User.Identity.Name;
- if (user.StartsWith("S-1-5-21")) //SID
+ var user = LdapIdentity.Parse(context.User.Identity.Name);
+ if (user.RawName.StartsWith("S-1-5-21")) //SID
{
- user = TryGetUpnFromSid(context.User.Identity);
+ user = LdapIdentity.Parse(TryGetUpnFromSid(context.User.Identity));
}
- var canonicalUserName = Util.CanonicalizeUserName(user);
- if (Constants.EXCHANGE_SYSTEM_MAILBOX_PREFIX.Any(sm => canonicalUserName.StartsWith(sm)))
+ if (Constants.EXCHANGE_SYSTEM_MAILBOX_PREFIX.Any(sm => user.Name.StartsWith(sm)))
{
//system mailbox
return;
@@ -86,8 +86,8 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
}
var ad = new ActiveDirectoryService(context.GetCacheAdapter(), Logger.Owa);
- var secondFactorRequired = new UserRequiredSecondFactor(ad);
- if (!secondFactorRequired.Execute(canonicalUserName))
+ var secondFactorRequired = new UserRequiredSecondFactor(ad, Logger.Owa);
+ if (!secondFactorRequired.Execute(user))
{
//bypass 2fa
return;
@@ -95,9 +95,9 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
//mfa
var valSrv = new TokenValidationService(Logger.Owa);
- var checker = new AuthChecker(context, valSrv);
- var isAuthenticatedByMultifactor = checker.IsAuthenticated(user);
- if (isAuthenticatedByMultifactor || context.HasApiUnreachableFlag())
+ var checker = new AuthChecker(context, valSrv, Logger.Owa);
+ var isAuthenticatedByMultifactor = checker.IsAuthenticated(user.RawName);
+ if (isAuthenticatedByMultifactor || context.HasApiUnreachableFlag(false))
{
return;
}
diff --git a/Owa/UserRequiredSecondFactor.cs b/Owa/UserRequiredSecondFactor.cs
index d14ca13..37d1b40 100644
--- a/Owa/UserRequiredSecondFactor.cs
+++ b/Owa/UserRequiredSecondFactor.cs
@@ -1,4 +1,5 @@
using MultiFactor.IIS.Adapter.Services;
+using MultiFactor.IIS.Adapter.Services.Ldap;
using System;
namespace MultiFactor.IIS.Adapter.Owa
@@ -6,17 +7,19 @@ namespace MultiFactor.IIS.Adapter.Owa
public class UserRequiredSecondFactor
{
private readonly ActiveDirectoryService _activeDirectory;
+ private readonly Logger _logger;
- public UserRequiredSecondFactor(ActiveDirectoryService activeDirectory)
+ public UserRequiredSecondFactor(ActiveDirectoryService activeDirectory, Logger logger)
{
_activeDirectory = activeDirectory ?? throw new ArgumentNullException(nameof(activeDirectory));
+ _logger = logger;
}
- public bool Execute(string samAccountName)
+ public bool Execute(LdapIdentity identity)
{
- if (samAccountName is null)
+ if (identity is null)
{
- throw new ArgumentNullException(nameof(samAccountName));
+ throw new ArgumentNullException(nameof(identity));
}
if (string.IsNullOrEmpty(Configuration.Current.ActiveDirectory2FaGroup))
@@ -24,7 +27,9 @@ public bool Execute(string samAccountName)
return true;
}
- return _activeDirectory.ValidateMembership(samAccountName);
+ // very noisy log, only for debug
+ //_logger.Info($"Start validate membership for {identity.RawName}");
+ return _activeDirectory.ValidateMembership(identity);
}
}
}
\ No newline at end of file
diff --git a/Services/AccessUrl.cs b/Services/AccessUrl.cs
index 7a18b24..37633af 100644
--- a/Services/AccessUrl.cs
+++ b/Services/AccessUrl.cs
@@ -1,4 +1,5 @@
using System;
+using MultiFactor.IIS.Adapter.Services.Ldap;
namespace MultiFactor.IIS.Adapter.Services
{
@@ -13,25 +14,26 @@ public AccessUrl(ActiveDirectoryService activeDirectory, MultiFactorApiClient ap
_api = api ?? throw new ArgumentNullException(nameof(api));
}
- public string Get(string rawUsername, string postbackUrl)
+ public string Get(LdapIdentity identity, string postbackUrl)
{
- var identity = Util.CanonicalizeUserName(rawUsername);
- Logger.API.Info($"Applying identity canonicalization: {rawUsername}->{identity}");
-
+
var profile = _activeDirectory.GetProfile(identity);
if (profile == null)
{
// redirect to (custom?) error page
- throw new Exception($"Profile {rawUsername} not found");
+ throw new Exception($"Profile {identity.RawName} not found");
}
+
+ var twoFAIdentity = identity.Name; // canonicalizated name
+ Logger.API.Info($"Applying identity canonicalization: {identity.RawName}->{twoFAIdentity}");
- if (Configuration.Current.HasTwoFaIdentityAttribute && !string.IsNullOrEmpty(profile.TwoFAIdentity))
+ if (Configuration.Current.HasTwoFaIdentityAttribute && !string.IsNullOrEmpty(profile.Custom2FAIdentity))
{
- Logger.API.Info($"Applying 2fa identity attribute: {identity}->{profile.TwoFAIdentity}");
- identity = profile.TwoFAIdentity;
+ Logger.API.Info($"Applying 2fa identity attribute: {identity.RawName}->{profile.Custom2FAIdentity}");
+ twoFAIdentity = profile.Custom2FAIdentity;
}
- var multiFactorAccessUrl = _api.CreateRequest(identity, rawUsername, postbackUrl, profile?.Phone);
+ var multiFactorAccessUrl = _api.CreateRequest(twoFAIdentity, identity.RawName, postbackUrl, profile?.Phone);
return multiFactorAccessUrl;
}
}
diff --git a/Services/ActiveDirectoryService.cs b/Services/ActiveDirectoryService.cs
index 09d4db3..62ab387 100644
--- a/Services/ActiveDirectoryService.cs
+++ b/Services/ActiveDirectoryService.cs
@@ -1,4 +1,5 @@
using MultiFactor.IIS.Adapter.Extensions;
+using MultiFactor.IIS.Adapter.Interop;
using MultiFactor.IIS.Adapter.Services.Ldap;
using MultiFactor.IIS.Adapter.Services.Ldap.Profile;
using System;
@@ -22,14 +23,14 @@ public ActiveDirectoryService(CacheAdapter cache, Logger logger)
_config = Configuration.Current;
}
- public ILdapProfile GetProfile(string samAccountName)
+ public ILdapProfile GetProfile(LdapIdentity identity)
{
- if (samAccountName is null)
+ if (identity is null)
{
- throw new ArgumentNullException(nameof(samAccountName));
+ throw new ArgumentNullException(nameof(identity));
}
- var profile = _cache.GetProfile(samAccountName);
+ var profile = _cache.GetProfile(identity.RawName);
if (profile != null)
{
return profile;
@@ -39,19 +40,31 @@ public ILdapProfile GetProfile(string samAccountName)
{
try
{
- _logger.Info($"Try load profile from {domain}");
- using (var adapter = LdapConnectionAdapter.Create(domain, _logger))
+ _logger.Info($"Try load {identity.RawName} profile from {domain}");
+ UserSearchContext searchContext;
+ if (identity.HasNetbiosName())
+ {
+ searchContext = new NetbiosService(_logger).ConvertToUpnUser(identity, domain);
+ }
+ else
+ {
+ _logger.Warn($"Something strange: user {identity.RawName} has not netbiosname. Identity: {identity.Name}, {identity.TypeName}, {identity.NetBiosName}");
+ searchContext = new UserSearchContext(domain, identity.Name, identity.RawName);
+ }
+
+ _logger.Info($"Start load user profile in context: {searchContext}");
+ using (var adapter = LdapConnectionAdapter.Create(searchContext.Domain, _logger))
{
- var loader = new ProfileLoader(adapter, _config);
- profile = loader.Load(samAccountName);
+ var loader = new ProfileLoader(adapter, _config, _logger);
+ profile = loader.Load(searchContext.UserIdentity);
if (profile == null)
{
continue;
}
- _logger.Info($"Profile loaded for user '{profile.SamAccountName}'");
+ _logger.Info($"Profile loaded for user '{profile.RawUserName}'");
- _cache.SetProfile(samAccountName, profile);
+ _cache.SetProfile(identity.RawName, profile);
return profile;
}
}
@@ -63,6 +76,8 @@ public ILdapProfile GetProfile(string samAccountName)
{
_logger.Error(ex.ToString());
}
+ // very noisy, only for debug
+ // _logger.Info($"Get profile iteration for {domain} finished");
}
return null;
}
@@ -70,21 +85,21 @@ public ILdapProfile GetProfile(string samAccountName)
///
/// Only group members should 2fa
///
- public bool ValidateMembership(string samAccountName)
+ public bool ValidateMembership(LdapIdentity identity)
{
- var cachedMembership = _cache.GetMembership(samAccountName);
+ var cachedMembership = _cache.GetMembership(identity.RawName);
if (cachedMembership != null)
{
return cachedMembership.Value;
}
- var isMember = ValidateMembershipInternal(samAccountName);
- _cache.SetMembership(samAccountName, isMember);
+ var isMember = ValidateMembershipInternal(identity);
+ _cache.SetMembership(identity.RawName, isMember);
return isMember;
}
- private bool ValidateMembershipInternal(string samAccountName)
+ private bool ValidateMembershipInternal(LdapIdentity identity)
{
var groupName = _config.ActiveDirectory2FaGroup;
@@ -111,14 +126,29 @@ private bool ValidateMembershipInternal(string samAccountName)
return true; //group not exists, let unknown result will be true
}
- var searchFilter = $"(&(sAMAccountName={samAccountName})(memberOf:1.2.840.113556.1.4.1941:={groupDn}))";
- var response = adapter.Search(baseDn, searchFilter, SearchScope.Subtree, "DistinguishedName");
+ _logger.Info($"Try validate membership {identity.RawName} profile from {domain} in {groupDn}");
+ UserSearchContext searchContext;
+ if (identity.HasNetbiosName())
+ {
+ searchContext = new NetbiosService(_logger).ConvertToUpnUser(identity, domain);
+ }
+ else
+ {
+ _logger.Warn($"Something strange: user {identity.RawName} has not netbiosname. Identity: {identity.Name}, {identity.TypeName}, {identity.NetBiosName}");
+ searchContext = new UserSearchContext(domain, identity.Name, identity.RawName);
+ }
+
+ var searchFilter = $"(&({searchContext.UserIdentity.TypeName}={searchContext.UserIdentity.Name})(memberOf:1.2.840.113556.1.4.1941:={groupDn}))";
+ var response = adapter.Search(baseDn, searchFilter, SearchScope.Subtree, true, "DistinguishedName");
if (response.Entries.Count != 0)
{
+ _logger.Info($"{identity.RawName} is member of {groupName}");
return true;
}
}
+ // very noisy, only for debug
+ // _logger.Info($"ValidateMembership iteration for {domain} finished");
}
return false;
}
@@ -140,10 +170,18 @@ private bool ValidateMembershipInternal(string samAccountName)
private string GetGroupDn(LdapConnectionAdapter adapter, string name, string baseDn)
{
var searchFilter = $"(&(objectCategory=group)(name={name}))";
- var response = adapter.Search(baseDn, searchFilter, SearchScope.Subtree, "DistinguishedName");
- return response.Entries.Count == 0
- ? null
- : response.Entries[0].DistinguishedName;
+ var response = adapter.Search(baseDn, searchFilter, SearchScope.Subtree, true, "DistinguishedName");
+ if(response.Entries.Count != 0)
+ {
+ var groupDn = response.Entries[0].DistinguishedName;
+ _logger.Info($"Group {name} was found:{groupDn}");
+ return groupDn;
+ }
+ else
+ {
+ _logger.Info($"Group {name} was not found");
+ return null;
+ }
}
}
}
\ No newline at end of file
diff --git a/Services/AuthChecker.cs b/Services/AuthChecker.cs
index 2e28e26..9b75ac9 100644
--- a/Services/AuthChecker.cs
+++ b/Services/AuthChecker.cs
@@ -1,4 +1,5 @@
-using System;
+using MultiFactor.IIS.Adapter.Services.Ldap;
+using System;
using System.Web;
namespace MultiFactor.IIS.Adapter.Services
@@ -7,11 +8,13 @@ public class AuthChecker
{
private readonly HttpContextBase _context;
private readonly TokenValidationService _validationService;
+ private readonly Logger _logger;
- public AuthChecker(HttpContextBase context, TokenValidationService validationService)
+ public AuthChecker(HttpContextBase context, TokenValidationService validationService, Logger logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
+ _logger = logger;
}
public bool IsAuthenticated(string rawUsername)
@@ -28,7 +31,8 @@ public bool IsAuthenticated(string rawUsername)
return false;
}
- return Util.CanonicalizeUserName(userName) == Util.CanonicalizeUserName(rawUsername);
+ _logger.Info($"Сomparison netbios name of local user {rawUsername} and mf user {userName}");
+ return rawUsername == userName;
}
}
}
\ No newline at end of file
diff --git a/Services/Ldap/LdapConnectionAdapter.cs b/Services/Ldap/LdapConnectionAdapter.cs
index 7d68af5..479c85b 100644
--- a/Services/Ldap/LdapConnectionAdapter.cs
+++ b/Services/Ldap/LdapConnectionAdapter.cs
@@ -42,7 +42,7 @@ public static LdapConnectionAdapter Create(string domain, Logger logger)
return new LdapConnectionAdapter(conn, new FullyQualifiedDomainName(domain), logger);
}
- public SearchResponse Search(string baseDn, string filter, SearchScope scope, params string[] attributes)
+ public SearchResponse Search(string baseDn, string filter, SearchScope scope, bool chaseRefs, params string[] attributes)
{
if (string.IsNullOrEmpty(baseDn))
{
@@ -61,6 +61,8 @@ public SearchResponse Search(string baseDn, string filter, SearchScope scope, pa
var searchRequest = new SearchRequest(baseDn, filter, scope, attributes);
+ _connection.SessionOptions.ReferralChasing = chaseRefs ? ReferralChasingOptions.All : ReferralChasingOptions.None;
+
_logger.Info($"Sending search request with params:\r\nbase={baseDn}\r\nfilter={filter}\r\nscope={scope}\r\nattributes={string.Join(";", attributes)}");
return (SearchResponse)_connection.SendRequest(searchRequest);
}
diff --git a/Services/Ldap/LdapIdentity.cs b/Services/Ldap/LdapIdentity.cs
new file mode 100644
index 0000000..5260df2
--- /dev/null
+++ b/Services/Ldap/LdapIdentity.cs
@@ -0,0 +1,96 @@
+using System;
+
+namespace MultiFactor.IIS.Adapter.Services.Ldap
+{
+ public class LdapIdentity
+ {
+ ///
+ /// Name we got from the authentication pipeline. Most often it is netbios
+ ///
+ public string RawName { get; private set; }
+
+ ///
+ /// Normalized name. Can be used to search in the AD and as 2fa identity by default
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// Most often it matches the RawName
+ ///
+ public string NetBiosName { get; private set; } = string.Empty;
+
+ ///
+ /// Type of name in terms of AD
+ ///
+ public IdentityType Type { get; set; }
+
+ ///
+ /// AD attribute name
+ ///
+ public string TypeName
+ {
+ get
+ {
+ switch (Type)
+ {
+ case IdentityType.SamAccountName:
+ return "sAMAccountName";
+ case IdentityType.UserPrincipalName:
+ return "userPrincipalName";
+ default:
+ return "name";
+ }
+ }
+ }
+
+ public static LdapIdentity Parse(string name)
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+
+ var identity = name.ToLower();
+ string netBiosName = string.Empty;
+ //remove DOMAIN\\ prefix
+ var type = IdentityType.SamAccountName;
+ var index = identity.IndexOf("\\");
+ if (index > 0)
+ {
+ type = IdentityType.SamAccountName;
+ netBiosName = identity.Substring(0, index);
+ identity = identity.Substring(index + 1);
+ }
+
+ // rare case
+ if (identity.Contains("@"))
+ {
+ type = IdentityType.UserPrincipalName;
+ }
+
+ return new LdapIdentity
+ {
+ RawName = name,
+ Name = identity,
+ Type = type,
+ NetBiosName = netBiosName,
+ };
+ }
+
+ public bool HasNetbiosName() => !string.IsNullOrEmpty(NetBiosName);
+
+ public LdapIdentity WithRawName(string rawName)
+ {
+ this.RawName = rawName;
+ return this;
+ }
+ }
+
+ // from System.DirectoryServices.AccountManagement
+ public enum IdentityType
+ {
+ /// The identity is a Security Account Manager (SAM) name.
+ SamAccountName,
+ /// The identity is a name.
+ Name,
+ /// The identity is a User Principal Name (UPN).
+ UserPrincipalName,
+ }
+}
\ No newline at end of file
diff --git a/Services/Ldap/NetbiosService.cs b/Services/Ldap/NetbiosService.cs
new file mode 100644
index 0000000..89c9ac7
--- /dev/null
+++ b/Services/Ldap/NetbiosService.cs
@@ -0,0 +1,45 @@
+using MultiFactor.IIS.Adapter.Interop;
+using System;
+
+namespace MultiFactor.IIS.Adapter.Services.Ldap
+{
+ public class NetbiosService
+ {
+ public readonly Logger _logger;
+
+ public NetbiosService(Logger logger)
+ {
+ _logger = logger;
+ }
+
+ public UserSearchContext ConvertToUpnUser(LdapIdentity user, string domain)
+ {
+ var upnUserName = ResolveUserByNetBios(user.RawName, user.NetBiosName, domain);
+ return upnUserName;
+ }
+
+ private UserSearchContext ResolveUserByNetBios(string fullUserName, string netBiosName, string domain)
+ {
+ _logger.Info($"Trying to resolve domain by netbios {netBiosName}, user: {fullUserName}.");
+ try
+ {
+ using (var nameTranslator = new NameTranslator(domain, _logger))
+ {
+ // first try a strict domain resolving method
+ var searchContext = nameTranslator.Translate(fullUserName);
+ if (!string.IsNullOrEmpty(searchContext.Domain))
+ {
+ _logger.Info($"Success find {searchContext} by {fullUserName}");
+ return searchContext;
+ }
+ throw new Exception("NetbiosName not found");
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.Warn($"Error during translate netbios name {fullUserName}");
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/Ldap/Profile/ILdapProfile.cs b/Services/Ldap/Profile/ILdapProfile.cs
index 350a27e..cc3207b 100644
--- a/Services/Ldap/Profile/ILdapProfile.cs
+++ b/Services/Ldap/Profile/ILdapProfile.cs
@@ -2,8 +2,9 @@
{
public interface ILdapProfile
{
- string SamAccountName { get; }
- string TwoFAIdentity { get; }
+ string RawUserName { get; }
+ string FriendlyUserName { get; }
+ string Custom2FAIdentity { get; }
string Phone { get; }
}
}
\ No newline at end of file
diff --git a/Services/Ldap/Profile/LdapProfile.cs b/Services/Ldap/Profile/LdapProfile.cs
index 31b00bd..aed7e48 100644
--- a/Services/Ldap/Profile/LdapProfile.cs
+++ b/Services/Ldap/Profile/LdapProfile.cs
@@ -6,13 +6,26 @@ namespace MultiFactor.IIS.Adapter.Services.Ldap.Profile
{
internal class LdapProfile : ILdapProfile
{
+ private readonly LdapIdentity _identity;
private readonly string _twoFaIdentityAttrName;
private readonly string[] _phoneAttrs;
private readonly Dictionary> _attrs = new Dictionary>(new AttributeKeyComparer());
- public string SamAccountName => GetAttr("sAMAccountName").First();
- public string TwoFAIdentity => GetAttr(_twoFaIdentityAttrName).FirstOrDefault();
+ ///
+ /// Name we got from the authentication pipeline. Most often it is netbios
+ ///
+ public string RawUserName => _identity.RawName;
+
+ ///
+ /// Normalized name. Can be used to search in the AD and as 2fa identity by default.
+ ///
+ public string FriendlyUserName => _identity.Name;
+
+ ///
+ /// Use only if corresponding setting is specified in the config
+ ///
+ public string Custom2FAIdentity => GetAttr(_twoFaIdentityAttrName).FirstOrDefault();
public string Phone
{
@@ -30,22 +43,22 @@ public string Phone
}
}
- public LdapProfile(string samAccountName, Configuration configuration)
+ public LdapProfile(LdapIdentity identity, Configuration configuration)
{
- if (string.IsNullOrWhiteSpace(samAccountName))
+ if (identity == null)
{
- throw new ArgumentException($"'{nameof(samAccountName)}' cannot be null or whitespace.", nameof(samAccountName));
+ throw new ArgumentException($"'{nameof(identity)}' cannot be null.", nameof(identity));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
-
- _attrs["sAMAccountName"] = new HashSet{ samAccountName };
+ _identity = identity;
+ _attrs[identity.TypeName] = new HashSet{ identity.Name };
_twoFaIdentityAttrName = configuration.HasTwoFaIdentityAttribute
? configuration.TwoFaIdentityAttribute
- : "sAMAccountName";
+ : identity.TypeName;
var phoneAttrs = new List
{
diff --git a/Services/Ldap/Profile/ProfileLoader.cs b/Services/Ldap/Profile/ProfileLoader.cs
index 61d4c5b..d41c659 100644
--- a/Services/Ldap/Profile/ProfileLoader.cs
+++ b/Services/Ldap/Profile/ProfileLoader.cs
@@ -10,21 +10,23 @@ public class ProfileLoader
{
private readonly LdapConnectionAdapter _adapter;
private readonly Configuration _config;
+ private readonly Logger _logger;
- public ProfileLoader(LdapConnectionAdapter adapter, Configuration config)
+ public ProfileLoader(LdapConnectionAdapter adapter, Configuration config, Logger logger)
{
_adapter = adapter ?? throw new ArgumentNullException(nameof(adapter));
_config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = logger;
}
- public ILdapProfile Load(string samAccountName)
+ public ILdapProfile Load(LdapIdentity user)
{
- if (string.IsNullOrWhiteSpace(samAccountName))
+ if (string.IsNullOrWhiteSpace(user.Name))
{
- throw new ArgumentException("Value cannot be null or whitespace.", nameof(samAccountName));
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(user));
}
- var profile = new LdapProfile(samAccountName, _config);
+ var profile = new LdapProfile(user, _config);
var queryAttributes = new List();
queryAttributes.AddRange(_config.PhoneAttributes);
@@ -33,15 +35,38 @@ public ILdapProfile Load(string samAccountName)
queryAttributes.Add(_config.TwoFaIdentityAttribute);
}
- var baseDn = _adapter.Domain.GetDn();
- var searchFilter = $"(&(sAMAccountName={samAccountName})(objectClass=user))";
+ var baseDn = _adapter.Domain.GetDn();
- var response = _adapter.Search(baseDn, searchFilter, SearchScope.Subtree, queryAttributes.ToArray());
- if (response.Entries.Count == 0)
+ var searchFilter = $"(&(objectClass=user)({user.TypeName}={user.Name}))";
+
+ //only this domain
+ var response = _adapter.Search(baseDn, searchFilter, SearchScope.Subtree,false, queryAttributes.ToArray());
+
+ if (response.Entries.Count != 0)
{
+ // very noisy log,only for debug
+ // _logger.Info($"Success search for {user.Name} in {baseDn} with filter {searchFilter}, {response.Entries.Count} entries");
+ FillProfile(response, queryAttributes, profile);
return profile;
}
+ //with ReferralChasing
+ response = _adapter.Search(baseDn, searchFilter, SearchScope.Subtree, true, queryAttributes.ToArray());
+
+ if (response.Entries.Count != 0)
+ {
+ // very noisy log,only for debug
+ // _logger.Info($"Success referral search for {user} in {baseDn} with filter {searchFilter}, {response.Entries.Count} entries");
+ FillProfile(response, queryAttributes, profile);
+ return profile;
+ }
+
+ _logger.Info($"User {user} was not found in {baseDn}");
+ return profile;
+ }
+
+ private static void FillProfile(SearchResponse response, List queryAttributes, LdapProfile profile)
+ {
var attributes = response.Entries[0].Attributes;
foreach (var attr in queryAttributes)
{
@@ -51,8 +76,6 @@ public ILdapProfile Load(string samAccountName)
profile.AddAttribute(attr, values);
}
-
- return profile;
}
}
}
\ No newline at end of file
diff --git a/Services/MfaApiRequestExecutor.cs b/Services/MfaApiRequestExecutor.cs
index dfe4cf8..e21d723 100644
--- a/Services/MfaApiRequestExecutor.cs
+++ b/Services/MfaApiRequestExecutor.cs
@@ -1,4 +1,5 @@
using MultiFactor.IIS.Adapter.Extensions;
+using MultiFactor.IIS.Adapter.Services.Ldap;
using System;
using System.Web;
@@ -19,18 +20,20 @@ public MfaApiRequestExecutor(HttpContextBase context, AccessUrl accessUrl, Logge
public void Execute(string postbackUrl, string appRootPath)
{
+ var identity = LdapIdentity.Parse(_context.User.Identity.Name);
try
{
- var multiFactorAccessUrl = _accessUrl.Get(_context.User.Identity.Name, postbackUrl);
+ _logger.Info($"Execute 2fa for {identity.RawName}");
+ var multiFactorAccessUrl = _accessUrl.Get(identity, postbackUrl);
_context.Response.Redirect(multiFactorAccessUrl, true);
}
catch (Exception ex) when (NeedToBypass(ex))
{
_logger.Warn(
- $"Bypassing the second factor for user '{_context.User.Identity.Name}' due to an API error '{ex}'. {Environment.NewLine}" +
+ $"Bypassing the second factor for user '{identity.RawName}' due to an API error '{ex}'. {Environment.NewLine}" +
$"Bypass session duration: {Configuration.Current.ApiLifeCheckInterval.TotalMinutes} min");
_context.GetCacheAdapter()
- .SetApiUnreachable(Util.CanonicalizeUserName(_context.User.Identity.Name), true);
+ .SetApiUnreachable(identity.RawName, true);
_context.Response.Redirect(appRootPath, true);
}
catch (Exception ex)
diff --git a/Services/TokenValidationService.cs b/Services/TokenValidationService.cs
index 5247d09..e733139 100644
--- a/Services/TokenValidationService.cs
+++ b/Services/TokenValidationService.cs
@@ -71,6 +71,9 @@ public string VerifyToken(string jwt)
//as is logged user without transformation
var rawUserName = json[Constants.RAW_USER_NAME_CLAIM] as string;
+
+ // very noisy, only for debug
+ // _logger.Info($"Token verification result: rawName={rawUserName} and sub={sub}");
return rawUserName ?? sub;
}