Skip to content

Commit b059294

Browse files
Feature/multidomain (#15)
### Release 23.07.2024 | Multidomain and custom identity attribute #### New - multidomain support: multiple domains can be specified via ';' ``` <add key="multifactor:active-directory-domain" value="domain1.local;domain2.local" /> ``` - Added the `use-attribute-as-identity` setting, which allows you to specify the attribute that will be used as an identifier when checking the second factor. SHOULD use the new setting instead of `use-upn-as-identity`. ```xml <!-- Use the specified attribute as the user identity when checking the second factor--> <add key="multifactor:use-attribute-as-identity" value="mail"/> ```
1 parent af09d06 commit b059294

19 files changed

Lines changed: 404 additions & 228 deletions

Configuration.cs

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using MultiFactor.IIS.Adapter.Services;
2+
using System;
23
using System.Collections.Specialized;
34
using System.Configuration;
45
using System.Linq;
@@ -7,22 +8,42 @@ namespace MultiFactor.IIS.Adapter
78
{
89
public class Configuration
910
{
10-
public string ApiKey { get; private set; }
11-
public string ApiSecret { get; private set; }
12-
public string ApiUrl { get; private set; }
13-
public string ApiProxy { get; private set; }
11+
private readonly string _activeDirectoryDomain;
12+
public string[] ActiveDirectoryDomains =>
13+
(_activeDirectoryDomain ?? string.Empty).Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
14+
.Distinct()
15+
.ToArray();
16+
17+
public string ApiUrl { get; }
18+
public string ApiKey { get; }
19+
public string ApiSecret { get; }
20+
public string ApiProxy { get; }
21+
1422
public bool BypassSecondFactorWhenApiUnreachable { get; private set; }
1523

1624
public string ActiveDirectory2FaGroup { get; private set; }
17-
public TimeSpan ActiveDirectoryCacheTimout { get; private set; }
18-
public TimeSpan ApiLifeCheckInterval { get; private set; }
19-
public bool UseUpnAsIdentity { get; private set; }
20-
public string[] PhoneAttributes { get; private set; } = new string[0];
25+
public TimeSpan ActiveDirectoryCacheTimout { get; private set; }
26+
public TimeSpan ApiLifeCheckInterval { get; private set; }
2127

28+
//Lookup for some attribute and use it for 2fa instead of uid
29+
public bool UseIdentityAttribute => !string.IsNullOrWhiteSpace(TwoFaIdentityAttribute);
30+
public string TwoFaIdentityAttribute { get; private set; }
31+
public string[] PhoneAttributes { get; private set; } = new string[0];
32+
33+
2234
private static readonly Lazy<Configuration> _current = new Lazy<Configuration>(Load);
2335
public static Configuration Current => _current.Value;
36+
37+
protected Configuration() {}
2438

25-
protected Configuration() { }
39+
private Configuration(string activeDirectoryDomain, string apiUrl, string apiKey, string apiSecret, string apiProxy)
40+
{
41+
_activeDirectoryDomain = activeDirectoryDomain;
42+
ApiUrl = apiUrl;
43+
ApiKey = apiKey;
44+
ApiSecret = apiSecret;
45+
ApiProxy = apiProxy;
46+
}
2647

2748
protected static Configuration Load()
2849
{
@@ -34,35 +55,31 @@ protected static Configuration Load()
3455
var apiProxySetting = appSettings[ConfigurationKeys.ApiProxy];
3556

3657
var activeDirectory2FaGroupSetting = appSettings[ConfigurationKeys.ActiveDirectory2FAGroup];
37-
var useUpnAsIdentitySetting = appSettings[ConfigurationKeys.UseUpnAsIdentity];
58+
var activeDirectoryDomain = appSettings[ConfigurationKeys.ActiveDirectoryDomain];
3859

39-
if (string.IsNullOrEmpty(apiUrlSetting))
60+
var domain = GetDomain(activeDirectoryDomain);
61+
62+
if (string.IsNullOrWhiteSpace(apiUrlSetting))
4063
{
4164
throw new Exception($"Configuration error: '{ConfigurationKeys.ApiUrl}' element not found or empty");
4265
}
43-
if (string.IsNullOrEmpty(apiKeySetting))
66+
if (string.IsNullOrWhiteSpace(apiKeySetting))
4467
{
4568
throw new Exception($"Configuration error: '{ConfigurationKeys.ApiKey}' element not found or empty");
4669
}
47-
if (string.IsNullOrEmpty(apiSecretSetting))
70+
if (string.IsNullOrWhiteSpace(apiSecretSetting))
4871
{
4972
throw new Exception($"Configuration error: '{ConfigurationKeys.ApiSecret}' element not found or empty");
5073
}
5174

52-
var config = new Configuration
53-
{
54-
ApiUrl = apiUrlSetting,
55-
ApiKey = apiKeySetting,
56-
ApiSecret = apiSecretSetting,
57-
ApiProxy = apiProxySetting,
58-
ActiveDirectory2FaGroup = activeDirectory2FaGroupSetting
59-
};
75+
var config = new Configuration(domain, apiUrlSetting, apiKeySetting, apiSecretSetting, apiProxySetting);
6076

61-
if (bool.TryParse(useUpnAsIdentitySetting, out var useUpnAsIdentity))
77+
if (!string.IsNullOrWhiteSpace(activeDirectory2FaGroupSetting))
6278
{
63-
config.UseUpnAsIdentity = useUpnAsIdentity;
79+
config.ActiveDirectory2FaGroup = activeDirectory2FaGroupSetting;
6480
}
6581

82+
ReadTwoFaIdentityAttributeSetting(appSettings, config);
6683
ReadActiveDirectoryCacheTimoutSetting(appSettings, config);
6784
ReadPhoneAttributeSetting(appSettings, config);
6885
ReadBypassWhenApiUnreachableSetting(appSettings, config);
@@ -71,6 +88,43 @@ protected static Configuration Load()
7188
return config;
7289
}
7390

91+
private static void ReadTwoFaIdentityAttributeSetting(NameValueCollection appSettings, Configuration configuration)
92+
{
93+
var useUpnAsIdentitySetting = appSettings[ConfigurationKeys.UseUpnAsIdentity];
94+
var twoFaIdentityAttributeSetting = appSettings[ConfigurationKeys.TwoFAIdentityAttribyte];
95+
96+
// MUST be before 'use-upn-as-identity' check
97+
if (!string.IsNullOrWhiteSpace(twoFaIdentityAttributeSetting))
98+
{
99+
configuration.TwoFaIdentityAttribute = twoFaIdentityAttributeSetting;
100+
}
101+
102+
//legacy settings for 2fa identity
103+
if (bool.TryParse(useUpnAsIdentitySetting, out var useUpnAsIdentity))
104+
{
105+
if (!string.IsNullOrWhiteSpace(twoFaIdentityAttributeSetting))
106+
{
107+
throw new Exception("Configuration error: Using settings 'use-upn-as-identity' and 'use-attribute-as-identity' together is unacceptable. Prefer using 'use-attribute-as-identity'.");
108+
}
109+
110+
Logger.Owa.Warn("The setting 'use-upn-as-identity' is deprecated, use 'use-attribute-as-identity' instead");
111+
if (useUpnAsIdentity)
112+
{
113+
configuration.TwoFaIdentityAttribute = "userPrincipalName";
114+
}
115+
}
116+
}
117+
118+
private static string GetDomain(string activeDirectoryDomain)
119+
{
120+
if (!string.IsNullOrWhiteSpace(activeDirectoryDomain))
121+
{
122+
return activeDirectoryDomain;
123+
}
124+
125+
return System.DirectoryServices.ActiveDirectory.Domain.GetComputerDomain().Name;
126+
}
127+
74128
private static void ReadPhoneAttributeSetting(NameValueCollection appSettings, Configuration configuration)
75129
{
76130
var value = appSettings[ConfigurationKeys.PhoneAttribute];
@@ -80,7 +134,7 @@ private static void ReadPhoneAttributeSetting(NameValueCollection appSettings, C
80134
}
81135

82136
var parsed = value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(attr => attr.Trim()).ToArray();
83-
if (parsed.Length != 0) configuration.PhoneAttributes = parsed;
137+
if (parsed.Length != 0) configuration.PhoneAttributes = parsed;
84138
}
85139

86140
private static void ReadActiveDirectoryCacheTimoutSetting(NameValueCollection appSettings, Configuration configuration)
@@ -89,13 +143,13 @@ private static void ReadActiveDirectoryCacheTimoutSetting(NameValueCollection ap
89143

90144
var legacyValue = appSettings[ConfigurationKeys.ActiveDirectory2FAGroupMembershipCacheTimeout];
91145
if (int.TryParse(legacyValue, out var legVal)) ttl = TimeSpan.FromMinutes(legVal);
92-
146+
93147
var value = appSettings[ConfigurationKeys.ActiveDirectoryCacheTimeout];
94148
if (int.TryParse(value, out var val)) ttl = TimeSpan.FromMinutes(val);
95-
149+
96150
configuration.ActiveDirectoryCacheTimout = ttl > TimeSpan.Zero ? ttl : TimeSpan.FromMinutes(15);
97151
}
98-
152+
99153
private static void ReadApiLifeCheckIntervalSetting(NameValueCollection appSettings, Configuration configuration)
100154
{
101155
var defaultValue = TimeSpan.FromMinutes(15);
@@ -114,7 +168,7 @@ private static void ReadApiLifeCheckIntervalSetting(NameValueCollection appSetti
114168

115169
configuration.ApiLifeCheckInterval = parsed > 0 ? TimeSpan.FromMinutes(parsed) : defaultValue;
116170
}
117-
171+
118172
private static void ReadBypassWhenApiUnreachableSetting(NameValueCollection appSettings, Configuration configuration)
119173
{
120174
var value = appSettings[ConfigurationKeys.BypassSecondFactorWhenApiUnreachable];

ConfigurationKeys.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ public static class ConfigurationKeys
1010
public static readonly string ApiProxy = $"{_prefix}:api-proxy";
1111
public static readonly string BypassSecondFactorWhenApiUnreachable = $"{_prefix}:bypass-second-factor-when-api-unreachable";
1212

13+
public static readonly string ActiveDirectoryDomain = $"{_prefix}:active-directory-domain";
1314
public static readonly string ActiveDirectory2FAGroup = $"{_prefix}:active-directory-2fa-group";
1415
public static readonly string ActiveDirectory2FAGroupMembershipCacheTimeout = $"{_prefix}:active-directory-2fa-group-membership-cache-timeout";
1516
public static readonly string ActiveDirectoryCacheTimeout = $"{_prefix}:active-directory-cache-timeout";
1617
public static readonly string ApiLifeCheckInterval = $"{_prefix}:api-life-check-interval";
1718

1819
public static readonly string UseUpnAsIdentity = $"{_prefix}:use-upn-as-identity";
20+
public static readonly string TwoFAIdentityAttribyte = $"{_prefix}:use-attribute-as-identity";
1921
public static readonly string PhoneAttribute = $"{_prefix}:phone-attribute";
2022
}
2123
}

Extensions/FullyQualifiedDomainNameExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public static string GetDn(this FullyQualifiedDomainName domain)
1717
if (domain is null) throw new ArgumentNullException(nameof(domain));
1818

1919
var name = domain.Value;
20-
var portIndex = domain.Value.IndexOf(":");
20+
var portIndex = domain.Value.IndexOf(":", StringComparison.Ordinal);
2121
if (portIndex > 0)
2222
{
2323
name = name.Substring(0, portIndex);

MsDynamics365/Module.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ private void ProcessMultifactorRequest(HttpContextBase context)
109109
return;
110110
}
111111

112-
var executor = MfaApiRequestExecutorFactory.CreateIIS(context);
112+
var executor = MfaApiRequestExecutorFactory.CreateCrm(context);
113113
executor.Execute(url, GetWebAppRoot());
114114
}
115115

@@ -126,7 +126,7 @@ private string GetWebAppRoot()
126126
context.Request.Url.Host :
127127
context.Request.Url.Authority;
128128

129-
host = string.Format("{0}://{1}", context.Request.Url.Scheme, host);
129+
host = $"{context.Request.Url.Scheme}://{host}";
130130

131131
if (context.Request.ApplicationPath != "/")
132132
{

MultiFactor.IIS.Adapter.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<Compile Include="MsDynamics365\Module.cs" />
7373
<Compile Include="Constants.cs" />
7474
<Compile Include="Core\HttpModuleBase.cs" />
75+
<Compile Include="Services\Ldap\Profile\AttributeKeyComparer.cs" />
7576
<Compile Include="Services\MfaApiRequestExecutor.cs" />
7677
<Compile Include="Services\MfaApiRequestExecutorFactory.cs" />
7778
<Compile Include="Owa\UserRequiredSecondFactor.cs" />
@@ -84,9 +85,9 @@
8485
<Compile Include="Extensions\FullyQualifiedDomainNameExtensions.cs" />
8586
<Compile Include="Services\Ldap\LdapConnectionAdapter.cs" />
8687
<Compile Include="Services\Ldap\Profile\ILdapProfile.cs" />
87-
<Compile Include="Services\Ldap\Profile\ILdapProfileBuilder.cs" />
8888
<Compile Include="Services\Ldap\Profile\LdapProfile.cs" />
8989
<Compile Include="Services\Logger.cs" />
90+
<Compile Include="Services\MfTraceIdFactory.cs" />
9091
<Compile Include="Services\MultiFactorApiClient.cs" />
9192
<Compile Include="Services\Ldap\Profile\ProfileLoader.cs" />
9293
<Compile Include="Services\AccessUrl.cs" />

Owa/Module.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using MultiFactor.IIS.Adapter.Extensions;
33
using MultiFactor.IIS.Adapter.Services;
44
using System;
5-
using System.IO;
65
using System.Linq;
76
using System.Security.Principal;
87
using System.Web;
@@ -39,6 +38,7 @@ public override void OnBeginRequest(HttpContextBase context)
3938
public override void OnPostAuthorizeRequest(HttpContextBase context)
4039
{
4140
var path = context.Request.Url.GetComponents(UriComponents.Path, UriFormat.Unescaped);
41+
4242
//static resources
4343
if (WebUtil.IsStaticResourceRequest(context.Request.Url))
4444
{
@@ -125,7 +125,6 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
125125

126126
private void ProcessMultifactorRequest(HttpContextBase context)
127127
{
128-
Logger.Owa.Info($"Process MFA request");
129128
//check if user session timed-out
130129
if (!context.User.Identity.IsAuthenticated)
131130
{

Services/AccessUrl.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,23 @@ public AccessUrl(ActiveDirectoryService activeDirectory, MultiFactorApiClient ap
1313
_api = api ?? throw new ArgumentNullException(nameof(api));
1414
}
1515

16-
public string Get(string rawUsername, string postbackUrl)
16+
public string Get(string rawUsername, string postbackUrl)
1717
{
1818
var identity = rawUsername;
1919
var profile = _activeDirectory.GetProfile(Util.CanonicalizeUserName(identity));
20-
if (Configuration.Current.UseUpnAsIdentity)
20+
if (profile == null)
2121
{
22-
if (!identity.Contains("@"))
23-
{
24-
identity = profile.UserPrincipalName;
25-
}
22+
// redirect to (custom?) error page
23+
throw new Exception($"Profile {rawUsername} not found");
24+
}
25+
if (Configuration.Current.UseIdentityAttribute && !string.IsNullOrEmpty(profile.TwoFAIdentity))
26+
{
27+
Logger.API.Info($"Applying 2fa identity attribute: {identity}->{profile.TwoFAIdentity}");
28+
identity = profile.TwoFAIdentity;
2629
}
2730

28-
var multiFactorAccessUrl = _api.CreateRequest(identity, rawUsername, postbackUrl, profile);
31+
var multiFactorAccessUrl = _api.CreateRequest(identity, rawUsername, postbackUrl, profile?.Phone);
2932
return multiFactorAccessUrl;
30-
}
33+
}
3134
}
3235
}

0 commit comments

Comments
 (0)