Skip to content

Commit 8cb10f6

Browse files
improve multidomain search (#17)
### Release 02.04.2025 | Add pre-Windows 2000 logon name support #### New - The user can use a pre-Windows 2000 logon name for authentication: "netbiosname/username" - Improved user search in subdomains and with alternative suffixes
1 parent 928cab0 commit 8cb10f6

20 files changed

+598
-81
lines changed

Extensions/HttpContextBaseExtensions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@ public static CacheAdapter GetCacheAdapter(this HttpContextBase httpContext)
1616
return new CacheAdapter(httpContext);
1717
}
1818

19-
public static bool HasApiUnreachableFlag(this HttpContextBase httpContext)
19+
public static bool HasApiUnreachableFlag(this HttpContextBase httpContext, bool trimName = true)
2020
{
2121
var name = httpContext?.User?.Identity?.Name;
2222
if (string.IsNullOrEmpty(name))
2323
{
2424
return false;
2525
}
2626

27-
return httpContext.GetCacheAdapter().GetApiUnreachable(Util.CanonicalizeUserName(name));
27+
// for owa we dont want trim username, because owa send netbiosname
28+
// but for compatibility with Ms365 we use flag
29+
if (trimName)
30+
return httpContext.GetCacheAdapter().GetApiUnreachable(Util.CanonicalizeUserName(name));
31+
else
32+
return httpContext.GetCacheAdapter().GetApiUnreachable(name);
2833
}
2934
}
3035
}

Interop/NameTranslator.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using MultiFactor.IIS.Adapter.Services;
2+
using System;
3+
using System.ComponentModel;
4+
using System.Runtime.InteropServices;
5+
using static MultiFactor.IIS.Adapter.Interop.NativeMethods;
6+
7+
namespace MultiFactor.IIS.Adapter.Interop
8+
{
9+
public class NameTranslator : IDisposable
10+
{
11+
private readonly SafeDsHandle _handle;
12+
private readonly Logger _logger;
13+
private readonly string _domain;
14+
15+
public NameTranslator(string domain, Logger logger)
16+
{
17+
_domain = domain;
18+
_logger = logger;
19+
uint res = DsBind(domain, null, out _handle);
20+
if (res != (uint)DS_NAME_ERROR.DS_NAME_NO_ERROR)
21+
{
22+
_logger.Warn($"Failed to bind to: {domain}");
23+
throw new Win32Exception((int)res);
24+
}
25+
}
26+
27+
public UserSearchContext Translate(string netbiosName)
28+
{
29+
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);
30+
if (err != (uint)DS_NAME_ERROR.DS_NAME_NO_ERROR)
31+
{
32+
_logger.Warn($"Failed to translate {netbiosName} in {_domain}");
33+
throw new Win32Exception((int)err);
34+
}
35+
36+
try
37+
{
38+
// Next convert the returned structure to managed environment
39+
DS_NAME_RESULT Result = (DS_NAME_RESULT)Marshal.PtrToStructure(pResult, typeof(DS_NAME_RESULT));
40+
var res = Result.Items;
41+
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)))
42+
{
43+
_logger.Warn($"Unexpected result of translation {netbiosName} in {_domain}");
44+
throw new System.Security.SecurityException("Unable to resolve user name.");
45+
}
46+
return new UserSearchContext(res[0].pDomain, res[0].pName, netbiosName);
47+
}
48+
finally
49+
{
50+
DsFreeNameResult(pResult);
51+
}
52+
53+
}
54+
55+
public void Dispose()
56+
{
57+
_handle.Dispose();
58+
}
59+
}
60+
}

Interop/NativeMethods.cs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace MultiFactor.IIS.Adapter.Interop
5+
{
6+
internal static class NativeMethods
7+
{
8+
private const string NTDSAPI = "ntdsapi.dll";
9+
10+
[DllImport(NTDSAPI, CharSet = CharSet.Auto)]
11+
public static extern uint DsBind(
12+
string DomainControllerName, // in, optional
13+
string DnsDomainName, // in, optional
14+
out SafeDsHandle phDS);
15+
16+
[DllImport(NTDSAPI, CharSet = CharSet.Auto)]
17+
public static extern uint DsCrackNames(
18+
SafeDsHandle hDS,
19+
DS_NAME_FLAGS flags,
20+
DS_NAME_FORMAT formatOffered,
21+
DS_NAME_FORMAT formatDesired,
22+
uint cNames,
23+
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPTStr, SizeParamIndex = 4)]
24+
string[] rpNames,
25+
out IntPtr ppResult);
26+
27+
[DllImport(NTDSAPI, CharSet = CharSet.Auto)]
28+
public static extern void DsFreeNameResult(IntPtr pResult /* DS_NAME_RESULT* */);
29+
30+
[DllImport(NTDSAPI, CharSet = CharSet.Auto)]
31+
public static extern uint DsUnBind(ref IntPtr phDS);
32+
33+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
34+
public struct DS_NAME_RESULT
35+
{
36+
public uint cItems;
37+
internal IntPtr rItems; // PDS_NAME_RESULT_ITEM
38+
public DS_NAME_RESULT_ITEM[] Items
39+
{
40+
get
41+
{
42+
if (rItems == IntPtr.Zero)
43+
{
44+
return new DS_NAME_RESULT_ITEM[0];
45+
}
46+
47+
var ResultArray = new DS_NAME_RESULT_ITEM[cItems];
48+
Type strType = typeof(DS_NAME_RESULT_ITEM);
49+
int stSize = Marshal.SizeOf(strType);
50+
IntPtr curptr;
51+
52+
for (uint i = 0; i < cItems; i++)
53+
{
54+
curptr = new IntPtr(rItems.ToInt64() + (i * stSize));
55+
ResultArray[i] = (DS_NAME_RESULT_ITEM)Marshal.PtrToStructure(curptr, strType);
56+
}
57+
return ResultArray;
58+
}
59+
}
60+
}
61+
62+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
63+
public struct DS_NAME_RESULT_ITEM
64+
{
65+
public DS_NAME_ERROR status;
66+
public string pDomain;
67+
public string pName;
68+
public override string ToString()
69+
{
70+
if (status == DS_NAME_ERROR.DS_NAME_NO_ERROR)
71+
{
72+
return pName;
73+
}
74+
75+
return null;
76+
}
77+
}
78+
79+
public enum DS_NAME_ERROR
80+
{
81+
DS_NAME_NO_ERROR = 0,
82+
// Generic processing error.
83+
DS_NAME_ERROR_RESOLVING = 1,
84+
// Couldn't find the name at all - or perhaps caller doesn't have
85+
// rights to see it.
86+
DS_NAME_ERROR_NOT_FOUND = 2,
87+
// Input name mapped to more than one output name.
88+
DS_NAME_ERROR_NOT_UNIQUE = 3,
89+
// Input name found, but not the associated output format.
90+
// Can happen if object doesn't have all the required attributes.
91+
DS_NAME_ERROR_NO_MAPPING = 4,
92+
// Unable to resolve entire name, but was able to determine which
93+
// domain object resides in. Thus DS_NAME_RESULT_ITEM?.pDomain
94+
// is valid on return.
95+
DS_NAME_ERROR_DOMAIN_ONLY = 5,
96+
// Unable to perform a purely syntactical mapping at the client
97+
// without going out on the wire.
98+
DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING = 6,
99+
// The name is from an external trusted forest.
100+
DS_NAME_ERROR_TRUST_REFERRAL = 7
101+
}
102+
103+
[Flags]
104+
public enum DS_NAME_FLAGS
105+
{
106+
DS_NAME_NO_FLAGS = 0x0,
107+
// Perform a syntactical mapping at the client (if possible) without
108+
// going out on the wire. Returns DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING
109+
// if a purely syntactical mapping is not possible.
110+
DS_NAME_FLAG_SYNTACTICAL_ONLY = 0x1,
111+
// Force a trip to the DC for evaluation, even if this could be
112+
// locally cracked syntactically.
113+
DS_NAME_FLAG_EVAL_AT_DC = 0x2,
114+
// The call fails if the DC is not a GC
115+
DS_NAME_FLAG_GCVERIFY = 0x4,
116+
// Enable cross forest trust referral
117+
DS_NAME_FLAG_TRUST_REFERRAL = 0x8
118+
}
119+
120+
public enum DS_NAME_FORMAT
121+
{
122+
// unknown name type
123+
DS_UNKNOWN_NAME = 0,
124+
// eg: CN=User Name,OU=Users,DC=Example,DC=Microsoft,DC=Com
125+
DS_FQDN_1779_NAME = 1,
126+
// eg: Example\UserN
127+
// Domain-only version includes trailing '\\'.
128+
DS_NT4_ACCOUNT_NAME = 2,
129+
// Probably "User Name" but could be something else. I.e. The
130+
// display name is not necessarily the defining RDN.
131+
DS_DISPLAY_NAME = 3,
132+
// obsolete - see #define later
133+
// DS_DOMAIN_SIMPLE_NAME = 4,
134+
// obsolete - see #define later
135+
// DS_ENTERPRISE_SIMPLE_NAME = 5,
136+
// String-ized GUID as returned by IIDFromString().
137+
// eg: {4fa050f0-f561-11cf-bdd9-00aa003a77b6}
138+
DS_UNIQUE_ID_NAME = 6,
139+
// eg: example.microsoft.com/software/user name
140+
// Domain-only version includes trailing '/'.
141+
DS_CANONICAL_NAME = 7,
142+
// eg: usern@example.microsoft.com
143+
DS_USER_PRINCIPAL_NAME = 8,
144+
// Same as DS_CANONICAL_NAME except that rightmost '/' is
145+
// replaced with '\n' - even in domain-only case.
146+
// eg: example.microsoft.com/software\nuser name
147+
DS_CANONICAL_NAME_EX = 9,
148+
// eg: www/www.microsoft.com@example.com - generalized service principal
149+
// names.
150+
DS_SERVICE_PRINCIPAL_NAME = 10,
151+
// This is the string representation of a SID. Invalid for formatDesired.
152+
// See sddl.h for SID binary <--> text conversion routines.
153+
// eg: S-1-5-21-397955417-626881126-188441444-501
154+
DS_SID_OR_SID_HISTORY_NAME = 11,
155+
// Pseudo-name format so GetUserNameEx can return the DNS domain name to
156+
// a caller. This level is not supported by the DS APIs.
157+
DS_DNS_DOMAIN_NAME = 12
158+
}
159+
}
160+
}

Interop/SafeDsHandle.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Runtime.ConstrainedExecution;
3+
using System.Runtime.InteropServices;
4+
5+
namespace MultiFactor.IIS.Adapter.Interop
6+
{
7+
public class SafeDsHandle : SafeHandle
8+
{
9+
public SafeDsHandle() : base(IntPtr.Zero, true) { }
10+
11+
public override bool IsInvalid
12+
{
13+
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
14+
[PrePrepareMethod]
15+
get { return (handle == IntPtr.Zero); }
16+
}
17+
18+
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
19+
[PrePrepareMethod]
20+
21+
protected override bool ReleaseHandle()
22+
{
23+
uint ret = NativeMethods.DsUnBind(ref handle);
24+
System.Diagnostics.Debug.WriteLineIf(ret != 0, "Error unbinding :\t" + ret.ToString());
25+
return ret != 0;
26+
}
27+
}
28+
}

Interop/UserSearchContext.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using MultiFactor.IIS.Adapter.Services.Ldap;
2+
using System;
3+
4+
namespace MultiFactor.IIS.Adapter.Interop
5+
{
6+
public class UserSearchContext
7+
{
8+
public string Domain { get; set; }
9+
public LdapIdentity UserIdentity { get; set; }
10+
11+
public UserSearchContext(string domain, string upn, string rawUserName)
12+
{
13+
if (string.IsNullOrWhiteSpace(domain))
14+
throw new ArgumentException("Value cannot be null or whitespace.", nameof(domain));
15+
if (string.IsNullOrWhiteSpace(upn))
16+
throw new ArgumentException("Value cannot be null or whitespace.", nameof(upn));
17+
Domain = domain;
18+
UserIdentity = LdapIdentity.Parse(upn).WithRawName(rawUserName);
19+
}
20+
21+
public override string ToString() => $"User:{UserIdentity.RawName}, UPN:{UserIdentity.Name}, Domain:{Domain}";
22+
}
23+
}

MsDynamics365/Module.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using MultiFactor.IIS.Adapter.Extensions;
33
using MultiFactor.IIS.Adapter.Owa;
44
using MultiFactor.IIS.Adapter.Services;
5+
using MultiFactor.IIS.Adapter.Services.Ldap;
56
using System;
67
using System.Web;
78

@@ -49,9 +50,8 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
4950
//not yet authenticated with login/pwd
5051
return;
5152
}
52-
var user = context.User.Identity.Name;
53+
var user = LdapIdentity.Parse(context.User.Identity.Name);
5354

54-
var canonicalUserName = Util.CanonicalizeUserName(user);
5555

5656
//process request or postback to/from MultiFactor
5757
if (path.Contains(Constants.MULTIFACTOR_PAGE))
@@ -65,17 +65,17 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
6565
}
6666

6767
var ad = new ActiveDirectoryService(context.GetCacheAdapter(), Logger.IIS);
68-
var secondFactorRequired = new UserRequiredSecondFactor(ad);
69-
if (!secondFactorRequired.Execute(canonicalUserName))
68+
var secondFactorRequired = new UserRequiredSecondFactor(ad, Logger.IIS);
69+
if (!secondFactorRequired.Execute(user))
7070
{
7171
//bypass 2fa
7272
return;
7373
}
7474

7575
//mfa
7676
var valSrv = new TokenValidationService(Logger.IIS);
77-
var checker = new AuthChecker(context, valSrv);
78-
var isAuthenticatedByMultifactor = checker.IsAuthenticated(user);
77+
var checker = new AuthChecker(context, valSrv, Logger.IIS);
78+
var isAuthenticatedByMultifactor = checker.IsAuthenticated(user.RawName);
7979
if (isAuthenticatedByMultifactor || context.HasApiUnreachableFlag())
8080
{
8181
return;

MultiFactor.IIS.Adapter.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
<Compile Include="Configuration.cs" />
7070
<Compile Include="ConfigurationKeys.cs" />
7171
<Compile Include="Extensions\HttpContextBaseExtensions.cs" />
72+
<Compile Include="Interop\NameTranslator.cs" />
73+
<Compile Include="Interop\NativeMethods.cs" />
74+
<Compile Include="Interop\SafeDsHandle.cs" />
75+
<Compile Include="Interop\UserSearchContext.cs" />
7276
<Compile Include="MsDynamics365\Module.cs" />
7377
<Compile Include="Constants.cs" />
7478
<Compile Include="Core\HttpModuleBase.cs" />
@@ -77,6 +81,8 @@
7781
<DesignTime>True</DesignTime>
7882
<DependentUpon>Resources.resx</DependentUpon>
7983
</Compile>
84+
<Compile Include="Services\Ldap\LdapIdentity.cs" />
85+
<Compile Include="Services\Ldap\NetbiosService.cs" />
8086
<Compile Include="Services\Ldap\Profile\AttributeKeyComparer.cs" />
8187
<Compile Include="Services\MfaApiRequestExecutor.cs" />
8288
<Compile Include="Services\MfaApiRequestExecutorFactory.cs" />

0 commit comments

Comments
 (0)