diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..621522c
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,176 @@
+root = true
+
+[*.{cs,vb}]
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_style = space
+indent_size = 4
+end_of_line = crlf
+max_line_length = 160
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_prefer_collection_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:warning
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_diagnostic.CA1851.severity = error
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = warning
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_readonly_field = true:error
+dotnet_style_predefined_type_for_member_access = true:silent
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_allow_statement_immediately_after_block_experimental = false:suggestion
+dotnet_style_allow_multiple_blank_lines_experimental = true:silent
+dotnet_code_quality_unused_parameters = all:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+insert_final_newline = true
+dotnet_diagnostic.CA1069.severity = warning
+
+[*.cs]
+
+# CA1822: Mark members as static
+resharper_redundant_empty_object_creation_argument_list_highlighting=error
+dotnet_diagnostic.CA1822.severity = none
+csharp_indent_labels = one_less_than_current
+csharp_space_around_binary_operators = before_and_after
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_braces = true:error
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_throw_expression = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+csharp_new_line_before_open_brace = all
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.private_or_internal_field_should_be__fieldname.severity = suggestion
+dotnet_naming_rule.private_or_internal_field_should_be__fieldname.symbols = private_or_internal_field
+dotnet_naming_rule.private_or_internal_field_should_be__fieldname.style = _fieldname
+
+# Symbol specifications
+
+dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
+dotnet_naming_symbols.private_or_internal_field.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style._fieldname.required_prefix = _
+dotnet_naming_style._fieldname.word_separator =
+dotnet_naming_style._fieldname.capitalization = camel_case
+dotnet_diagnostic.xUnit2008.severity = error
+dotnet_diagnostic.xUnit2009.severity = error
+dotnet_diagnostic.ASP0000.severity = error
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_prefer_static_local_function = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
+csharp_style_allow_embedded_statements_on_same_line_experimental = false:error
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
+csharp_style_prefer_switch_expression = false:silent
+csharp_style_conditional_delegate_call = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+csharp_style_var_elsewhere = false:silent
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+csharp_space_before_dot = false
+csharp_space_after_dot = false
+dotnet_diagnostic.xUnit2000.severity = error
+
+#### Custom rules ####
+dotnet_diagnostic.Multifactor_EnforceNamedArgumentsForLiterals.severity = warning
diff --git a/ActiveSync/Module.cs b/ActiveSync/Module.cs
new file mode 100644
index 0000000..b3e54b2
--- /dev/null
+++ b/ActiveSync/Module.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Security.Principal;
+using System.Web;
+using MultiFactor.IIS.Adapter.Extensions;
+using MultiFactor.IIS.Adapter.Owa;
+using MultiFactor.IIS.Adapter.Services;
+using System.Runtime.Caching;
+using MultiFactor.IIS.Adapter.Core;
+
+namespace MultiFactor.IIS.Adapter.ActiveSync
+{
+ public class Module : IHttpModule
+ {
+ private MemoryCache _memoryCache;
+ private readonly string _applicationName = "/microsoft-server-activesync";
+ private HttpApplication _httpApplication;
+ public void Init(HttpApplication context)
+ {
+ context.PostAuthorizeRequest += HandlePostAuthorizeRequest;
+
+ _memoryCache = MemoryCache.Default;
+ _httpApplication = context;
+ }
+
+ private void HandlePostAuthorizeRequest(object sender, EventArgs e)
+ {
+ OnPostAuthorizeRequest(new HttpContextWrapper(((HttpApplication)sender).Context));
+ }
+
+ private void OnPostAuthorizeRequest(HttpContextBase context)
+ {
+ var path = context.Request.Url?.GetComponents(UriComponents.Path, UriFormat.Unescaped);
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return;
+ }
+
+ if (context.Request.ApplicationPath?.ToLower().StartsWith(_applicationName) != true)
+ {
+ return;
+ }
+
+ //static resources
+ if (WebUtil.IsStaticResourceRequest(context.Request.Url))
+ {
+ return;
+ }
+
+ if (!context.User.Identity.IsAuthenticated)
+ //not yet authenticated with login/pwd
+ {
+ return;
+ }
+
+ var user = context.User.Identity.Name;
+ if (user.StartsWith("S-1-5-21")) //SID
+ {
+ user = TryGetUpnFromSid(context.User.Identity);
+ }
+
+ var canonicalUserName = Util.CanonicalizeUserName(user);
+ if (Constants.EXCHANGE_SYSTEM_MAILBOX_PREFIX.Any(sm => canonicalUserName.StartsWith(sm)))
+ //system mailbox
+ {
+ return;
+ }
+
+ var ad = new ActiveDirectoryService(context.GetCacheAdapter(), Logger.ActiveSync);
+ var secondFactorRequired = new UserRequiredSecondFactor(ad);
+ if (!secondFactorRequired.Execute(canonicalUserName))
+ //bypass 2fa
+ {
+ return;
+ }
+
+ if (WebUtil.IsXhrRequest(context.Request))
+ {
+ AccessDenied(context);
+ return;
+ }
+
+ if (WebUtil.IsInitialProvisionRequest(context.Request))
+ {
+ var cacheKey = BuildCacheKey(context);
+ var cachedValue = _memoryCache.Get(cacheKey);
+ var val = cachedValue as int?;
+
+ if (val == 0)
+ {
+ AccessDenied(context);
+ return;
+ }
+
+ if (val == 1 || context.HasApiUnreachableFlag())
+ {
+ return;
+ }
+
+ //call to mfa
+ var secondFactorIsSuccessed = StartSecondFactorAuth(context);
+ if (secondFactorIsSuccessed)
+ {
+ _memoryCache.Set(cacheKey, 1, DateTimeOffset.Now.AddSeconds(5));
+ return;
+ }
+
+ _memoryCache.Set(cacheKey, 0, DateTimeOffset.Now.AddSeconds(5));
+ AccessDenied(context);
+ }
+ }
+
+ private bool StartSecondFactorAuth(HttpContextBase context)
+ {
+ var ad = new ActiveDirectoryService(context.GetCacheAdapter(), Logger.ActiveSync);
+
+ var userName = context.User.Identity.Name;
+
+ var identity = Util.CanonicalizeUserName(userName);
+ Logger.ActiveSync.Info($"Applying identity canonicalization: {userName}->{identity}");
+
+ var profile = ad.GetProfile(identity);
+
+ if (profile == null)
+ {
+ Logger.ActiveSync.Error($"No profile found for identity: {identity}");
+ // redirect to (custom?) error page
+ throw new Exception($"Profile {identity} not found");
+ }
+
+ if (Configuration.Current.HasTwoFaIdentityAttribute && !string.IsNullOrEmpty(profile.TwoFAIdentity))
+ {
+ Logger.ActiveSync.Info($"Applying 2fa identity attribute: {identity}->{profile.TwoFAIdentity}");
+ identity = profile.TwoFAIdentity;
+ }
+
+ var response = CreateAccessRequest(context, identity, profile?.Phone, profile?.Email);
+
+ return response.Granted;
+ }
+
+ private MultiFactorAccessRequest CreateAccessRequest(HttpContextBase context, string identity, string phone, string email)
+ {
+ var api = new MultiFactorApiClient(Logger.ActiveSync, MfTraceIdFactory.CreateTraceActiveSync);
+
+ try
+ {
+ var response = api.CreateNonInteractiveAccessRequest("/access/requests/ex", new PersonalData(identity, email, phone));
+ return response;
+ }
+ catch (WebException wex) when (Configuration.Current.BypassSecondFactorWhenApiUnreachable)
+ {
+ var errmsg = $"Multifactor API host unreachable: {Configuration.Current.ApiUrl}. Reason: {wex}";
+ Logger.ActiveSync.Error(errmsg);
+
+ if (wex.Response != null)
+ {
+ var httpStatusCode = ((HttpWebResponse)wex.Response).StatusCode;
+ if ((int)httpStatusCode == 429)
+ {
+ Logger.ActiveSync.Error($"Too many requests. Please try again later.");
+ }
+
+ return new MultiFactorAccessRequest { Status = "Denied" };
+ }
+
+ Logger.ActiveSync.Warn($"Bypassing the second factor for user '{identity}'.");
+ context
+ .GetCacheAdapter()
+ .SetApiUnreachable(Util.CanonicalizeUserName(identity), true);
+ return new MultiFactorAccessRequest { Status = "Granted" };
+ }
+ catch (Exception ex)
+ {
+ Logger.ActiveSync.Error(ex.ToString());
+ }
+
+ return new MultiFactorAccessRequest { Status = "Denied" };
+ }
+
+ public static string TryGetUpnFromSid(IIdentity identity)
+ {
+ //for download domains exchange uses OAuthIdentity with SID name
+ //lets try find UPN with reflection
+ var actAsUser = GetPropValue(identity, "ActAsUser");
+ var upn = GetPropValue(actAsUser, "UserPrincipalName");
+ return upn as string;
+ }
+
+ public static object GetPropValue(object src, string propName)
+ {
+ try
+ {
+ if (src == null)
+ {
+ return null;
+ }
+
+ return src.GetType().GetProperty(propName).GetValue(src, null);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private void AccessDenied(HttpContextBase context)
+ {
+ context.Response.StatusCode = 440;
+ context.Response.End();
+ }
+
+ private string BuildCacheKey(HttpContextBase context)
+ {
+ var userName = context.User.Identity.Name;
+ var deviceId = context.Request.Params["DeviceId"];
+ return $"{userName}-{deviceId}";
+ }
+
+ public void Dispose()
+ {
+ _httpApplication.PostAuthorizeRequest -= HandlePostAuthorizeRequest;
+ }
+ }
+}
diff --git a/Core/PersonalData.cs b/Core/PersonalData.cs
new file mode 100644
index 0000000..5b2cb88
--- /dev/null
+++ b/Core/PersonalData.cs
@@ -0,0 +1,16 @@
+namespace MultiFactor.IIS.Adapter.Core
+{
+ public class PersonalData
+ {
+ public string Email { get; }
+ public string Phone { get; }
+ public string Identity { get; }
+
+ public PersonalData(string identity, string email, string phone)
+ {
+ Identity = identity;
+ Email = email;
+ Phone = phone;
+ }
+ }
+}
diff --git a/HttpContextBaseExtensions.cs b/HttpContextBaseExtensions.cs
new file mode 100644
index 0000000..b18ef36
--- /dev/null
+++ b/HttpContextBaseExtensions.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Web;
+
+namespace MultiFactor.IIS.Adapter
+{
+ public static class HttpContextBaseExtensions
+ {
+ public static void RemoveCookie(
+ this HttpContextBase context,
+ string cookieName)
+ {
+ HttpCookie cookie = context.Response.Cookies[cookieName];
+ if (cookie == null)
+ {
+ return;
+ }
+
+ cookie.Expires = DateTime.UtcNow.AddDays(-1);
+ }
+
+ public static void AddCookie(this HttpContextBase context, HttpCookie cookie)
+ {
+ context.Response.Cookies.Set(cookie);
+ }
+
+ public static void RemoveAllCookie(this HttpContextBase context)
+ {
+ foreach (string allKey in context.Request.Cookies.AllKeys)
+ {
+ context.RemoveCookie(allKey);
+ }
+ }
+ }
+}
diff --git a/MultiFactor.IIS.Adapter.Tests/MultiFactor.IIS.Adapter.Tests.csproj b/MultiFactor.IIS.Adapter.Tests/MultiFactor.IIS.Adapter.Tests.csproj
index db07d53..c29661d 100644
--- a/MultiFactor.IIS.Adapter.Tests/MultiFactor.IIS.Adapter.Tests.csproj
+++ b/MultiFactor.IIS.Adapter.Tests/MultiFactor.IIS.Adapter.Tests.csproj
@@ -53,7 +53,6 @@
-
diff --git a/MultiFactor.IIS.Adapter.Tests/OwaModuleTests.cs b/MultiFactor.IIS.Adapter.Tests/OwaModuleTests.cs
deleted file mode 100644
index 17cb84b..0000000
--- a/MultiFactor.IIS.Adapter.Tests/OwaModuleTests.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Moq;
-using System;
-using Xunit;
-
-namespace MultiFactor.IIS.Adapter.Tests
-{
- public class OwaModuleTests
- {
- [Fact]
- public void OnBeginRequest_ContainsToken_ShouldSetupCookieAndRedirect()
- {
- const string expectedRedirect = "https://exchange/owa";
- var context = HttpContextMockBuilder.Create(x =>
- {
- x.Request.SetupGet(r => r.Url).Returns(new Uri("https://exchange/owa/mfa.aspx"));
- x.Request.SetupGet(r => r.ApplicationPath).Returns(expectedRedirect);
- x.Form.Add("AccessToken", "MFA TOKEN");
- });
- var module = new Owa.Module();
-
- module.OnBeginRequest(context.Object);
-
- Assert.Single(context.Object.Response.Cookies.AllKeys, Constants.COOKIE_NAME);
- context.Verify(x => x.Response.Redirect(expectedRedirect, true), Times.Once);
- }
- }
-}
diff --git a/MultiFactor.IIS.Adapter.csproj b/MultiFactor.IIS.Adapter.csproj
index 86d1f5d..a52410d 100644
--- a/MultiFactor.IIS.Adapter.csproj
+++ b/MultiFactor.IIS.Adapter.csproj
@@ -61,17 +61,22 @@
+
+
+
+
+
True
True
@@ -82,7 +87,6 @@
-
@@ -96,6 +100,8 @@
+
+
diff --git a/Owa/Module.cs b/Owa/Module.cs
index 875975a..5507836 100644
--- a/Owa/Module.cs
+++ b/Owa/Module.cs
@@ -1,18 +1,29 @@
-using MultiFactor.IIS.Adapter.Core;
using MultiFactor.IIS.Adapter.Extensions;
-using MultiFactor.IIS.Adapter.Properties;
using MultiFactor.IIS.Adapter.Services;
using System;
using System.Linq;
+using System.Runtime.Caching;
using System.Security.Principal;
using System.Web;
namespace MultiFactor.IIS.Adapter.Owa
{
- public class Module : HttpModuleBase
+ public class Module : IHttpModule
{
- public override void OnBeginRequest(HttpContextBase context)
+ private MemoryCache _memoryCache;
+ private HttpApplication _httpApplication;
+
+ public void Init(HttpApplication context)
+ {
+ _httpApplication = context;
+ _memoryCache = MemoryCache.Default;
+ context.BeginRequest += OnBeginRequest;
+ context.PostAuthorizeRequest += OnPostAuthorizeRequest;
+ }
+
+ public void OnBeginRequest(object sender, EventArgs e)
{
+ HttpContextBase context = new HttpContextWrapper(((HttpApplication)sender).Context);
var path = context.Request.Url.GetComponents(UriComponents.Path, UriFormat.Unescaped);
if (path.Contains("lang.owa"))
{
@@ -25,12 +36,14 @@ public override void OnBeginRequest(HttpContextBase context)
return;
}
- var complete2FaHtml = Resources.complete_2fa_html.Replace("%MULTIFACTOR_COOKIE%", token);
- SendPage(context, complete2FaHtml);
+ var cookie = new HttpCookie(BuildCookieName(context), token) { Path = context.Request.ApplicationPath };
+ context.AddCookie(cookie);
+ context.Response.Redirect(context.Request.ApplicationPath ?? "/");
}
- public override void OnPostAuthorizeRequest(HttpContextBase context)
+ public void OnPostAuthorizeRequest(object sender, EventArgs e)
{
+ HttpContextBase context = new HttpContextWrapper(((HttpApplication)sender).Context);
var path = context.Request.Url.GetComponents(UriComponents.Path, UriFormat.Unescaped);
//static resources
@@ -40,9 +53,17 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
}
//logoff page
- if (path.Contains("logoff.owa"))
+ if (path.ToLower().Contains("logoff"))
{
- SendPage(context, Resources.complete_logout_html);
+ var cookieName = BuildCookieName(context);
+ var cookie = context.Request.Cookies[cookieName];
+ if (cookie != null)
+ {
+ context.RemoveCookie(cookieName);
+ _memoryCache.Remove(cookie?.Value);
+ }
+
+ context.Response.Redirect(context.Request.ApplicationPath);
return;
}
@@ -53,9 +74,12 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
}
//language selection page
- if (path.Contains("languageselection.aspx") || path.Contains("lang.owa")) return;
+ if (path.Contains("languageselection.aspx") || path.Contains("lang.owa"))
+ {
+ return;
+ }
- if (!context.User.Identity.IsAuthenticated)
+ if (context.User?.Identity?.IsAuthenticated != true)
{
//not yet authenticated with login/pwd
return;
@@ -79,7 +103,24 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
{
if (context.Request.HttpMethod == "POST")
{
- ProcessMultifactorRequest(context);
+ string identityKey = context.Request.Cookies["identity"]?.Value;
+
+ if (string.IsNullOrWhiteSpace(identityKey))
+ {
+ AccessDenied(context);
+ }
+
+ var identity = _memoryCache.Get(identityKey)?.ToString();
+
+ if (string.IsNullOrWhiteSpace(identity))
+ {
+ AccessDenied(context);
+ }
+
+ context.RemoveCookie("identity");
+ _memoryCache.Remove(identityKey);
+
+ ProcessMultifactorRequest(context, identity);
}
return;
@@ -96,7 +137,7 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
//mfa
var valSrv = new TokenValidationService(Logger.Owa);
var checker = new AuthChecker(context, valSrv);
- var isAuthenticatedByMultifactor = checker.IsAuthenticated(user);
+ var isAuthenticatedByMultifactor = checker.IsAuthenticated(user, BuildCookieName(context));
if (isAuthenticatedByMultifactor || context.HasApiUnreachableFlag())
{
return;
@@ -112,20 +153,18 @@ public override void OnPostAuthorizeRequest(HttpContextBase context)
}
//redirect to mfa
- var redirectUrl = $"{context.Request.ApplicationPath}/{Constants.MULTIFACTOR_PAGE}";
- context.Response.Redirect(redirectUrl);
- }
-
- private void SendPage(HttpContextBase context, string html)
- {
- context.Response.Clear();
- context.Response.ClearContent();
- context.Response.Write(html);
- context.Response.Flush();
- context.Response.End();
+ if (canonicalUserName != "system")
+ {
+ var redirectUrl = $"{context.Request.ApplicationPath}/{Constants.MULTIFACTOR_PAGE}";
+ var cookieName = "identity";
+ var cacheKey = Guid.NewGuid().ToString();
+ _memoryCache.Set(cacheKey, canonicalUserName, DateTime.Now.AddMinutes(6));
+ context.AddCookie(new HttpCookie(cookieName, cacheKey) { Expires = DateTime.Now.AddMinutes(5) });
+ context.Response.Redirect(redirectUrl);
+ }
}
- private void ProcessMultifactorRequest(HttpContextBase context)
+ private void ProcessMultifactorRequest(HttpContextBase context, string forceIdentity = null)
{
//check if user session timed-out
if (!context.User.Identity.IsAuthenticated)
@@ -140,13 +179,7 @@ private void ProcessMultifactorRequest(HttpContextBase context)
return;
}
- //mfa request
- if (url.IndexOf("#") == -1)
- {
- url += "#path=/mail";
- }
-
- var executor = MfaApiRequestExecutorFactory.CreateOwa(context);
+ var executor = MfaApiRequestExecutorFactory.CreateOwa(context, forceIdentity);
executor.Execute(url, context.Request.ApplicationPath);
}
@@ -175,5 +208,22 @@ public static object GetPropValue(object src, string propName)
return null;
}
}
+
+ private void AccessDenied(HttpContextBase context)
+ {
+ context.Response.StatusCode = 440;
+ context.Response.End();
+ }
+
+ private string BuildCookieName(HttpContextBase context)
+ {
+ return Constants.COOKIE_NAME + context.Request.ApplicationPath;
+ }
+
+ public void Dispose()
+ {
+ _httpApplication.BeginRequest -= OnBeginRequest;
+ _httpApplication.PostAuthorizeRequest -= OnPostAuthorizeRequest;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Services/AuthChecker.cs b/Services/AuthChecker.cs
index 2e28e26..9be381f 100644
--- a/Services/AuthChecker.cs
+++ b/Services/AuthChecker.cs
@@ -14,9 +14,9 @@ public AuthChecker(HttpContextBase context, TokenValidationService validationSer
_validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
}
- public bool IsAuthenticated(string rawUsername)
+ public bool IsAuthenticated(string rawUsername, string cookieName = Constants.COOKIE_NAME)
{
- var multifactorCookie = _context.Request.Cookies[Constants.COOKIE_NAME];
+ var multifactorCookie = _context.Request.Cookies[cookieName];
if (multifactorCookie == null)
{
return false;
@@ -31,4 +31,4 @@ public bool IsAuthenticated(string rawUsername)
return Util.CanonicalizeUserName(userName) == Util.CanonicalizeUserName(rawUsername);
}
}
-}
\ No newline at end of file
+}
diff --git a/Services/Ldap/Profile/ILdapProfile.cs b/Services/Ldap/Profile/ILdapProfile.cs
index 350a27e..1b3d118 100644
--- a/Services/Ldap/Profile/ILdapProfile.cs
+++ b/Services/Ldap/Profile/ILdapProfile.cs
@@ -5,5 +5,6 @@ public interface ILdapProfile
string SamAccountName { get; }
string TwoFAIdentity { get; }
string Phone { get; }
+ string Email { get; }
}
-}
\ No newline at end of file
+}
diff --git a/Services/Ldap/Profile/LdapProfile.cs b/Services/Ldap/Profile/LdapProfile.cs
index 31b00bd..ee04740 100644
--- a/Services/Ldap/Profile/LdapProfile.cs
+++ b/Services/Ldap/Profile/LdapProfile.cs
@@ -30,6 +30,14 @@ public string Phone
}
}
+ public string Email
+ {
+ get
+ {
+ return GetAttr("mail").FirstOrDefault() ?? GetAttr("email").FirstOrDefault();
+ }
+ }
+
public LdapProfile(string samAccountName, Configuration configuration)
{
if (string.IsNullOrWhiteSpace(samAccountName))
@@ -101,4 +109,4 @@ private string[] GetAttr(string key)
: new string[0];
}
}
-}
\ No newline at end of file
+}
diff --git a/Services/Ldap/Profile/ProfileLoader.cs b/Services/Ldap/Profile/ProfileLoader.cs
index 61d4c5b..5a50ee9 100644
--- a/Services/Ldap/Profile/ProfileLoader.cs
+++ b/Services/Ldap/Profile/ProfileLoader.cs
@@ -28,6 +28,8 @@ public ILdapProfile Load(string samAccountName)
var queryAttributes = new List();
queryAttributes.AddRange(_config.PhoneAttributes);
+ queryAttributes.Add("mail");
+ queryAttributes.Add("email");
if (_config.HasTwoFaIdentityAttribute)
{
queryAttributes.Add(_config.TwoFaIdentityAttribute);
@@ -55,4 +57,4 @@ public ILdapProfile Load(string samAccountName)
return profile;
}
}
-}
\ No newline at end of file
+}
diff --git a/Services/Logger.cs b/Services/Logger.cs
index d373efb..70efc97 100644
--- a/Services/Logger.cs
+++ b/Services/Logger.cs
@@ -18,7 +18,8 @@ public Logger(string source)
public static Logger Owa => new Logger("Multifactor OWA");
public static Logger API => new Logger("Multifactor API");
public static Logger IIS => new Logger("Multifactor IIS");
-
+ public static Logger ActiveSync => new Logger("Multifactor ActiveSync");
+
public void Info(string message)
{
WriteEvent(message, EventLogEntryType.Information);
diff --git a/Services/MfTraceIdFactory.cs b/Services/MfTraceIdFactory.cs
index 20fe469..af61302 100644
--- a/Services/MfTraceIdFactory.cs
+++ b/Services/MfTraceIdFactory.cs
@@ -6,5 +6,6 @@ public static class MfTraceIdFactory
{
public static string CreateTraceOwa() => $"iis-owa-{Guid.NewGuid()}";
public static string CreateTraceCrm() => $"iis-crm-{Guid.NewGuid()}";
+ public static string CreateTraceActiveSync() => $"iis-eas-{Guid.NewGuid()}";
}
-}
\ No newline at end of file
+}
diff --git a/Services/MfaApiRequestExecutor.cs b/Services/MfaApiRequestExecutor.cs
index dfe4cf8..c7f346e 100644
--- a/Services/MfaApiRequestExecutor.cs
+++ b/Services/MfaApiRequestExecutor.cs
@@ -9,19 +9,21 @@ internal class MfaApiRequestExecutor
private readonly HttpContextBase _context;
private readonly AccessUrl _accessUrl;
private readonly Logger _logger;
+ private readonly string _forcedIdentity;
- public MfaApiRequestExecutor(HttpContextBase context, AccessUrl accessUrl, Logger logger)
+ public MfaApiRequestExecutor(HttpContextBase context, AccessUrl accessUrl, Logger logger, string forcedIdentity = null)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_accessUrl = accessUrl ?? throw new ArgumentNullException(nameof(accessUrl));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _forcedIdentity = forcedIdentity;
}
public void Execute(string postbackUrl, string appRootPath)
{
try
{
- var multiFactorAccessUrl = _accessUrl.Get(_context.User.Identity.Name, postbackUrl);
+ var multiFactorAccessUrl = _accessUrl.Get(_forcedIdentity ?? _context.User.Identity.Name, postbackUrl);
_context.Response.Redirect(multiFactorAccessUrl, true);
}
catch (Exception ex) when (NeedToBypass(ex))
diff --git a/Services/MfaApiRequestExecutorFactory.cs b/Services/MfaApiRequestExecutorFactory.cs
index eaa6670..a87b633 100644
--- a/Services/MfaApiRequestExecutorFactory.cs
+++ b/Services/MfaApiRequestExecutorFactory.cs
@@ -6,7 +6,7 @@ namespace MultiFactor.IIS.Adapter.Services
{
internal static class MfaApiRequestExecutorFactory
{
- public static MfaApiRequestExecutor CreateOwa(HttpContextBase context)
+ public static MfaApiRequestExecutor CreateOwa(HttpContextBase context, string forcedIdentity = null)
{
if (context is null)
{
@@ -17,7 +17,7 @@ public static MfaApiRequestExecutor CreateOwa(HttpContextBase context)
var api = new MultiFactorApiClient(Logger.API, MfTraceIdFactory.CreateTraceOwa);
var getter = new AccessUrl(ad, api);
- return new MfaApiRequestExecutor(context, getter, Logger.Owa);
+ return new MfaApiRequestExecutor(context, getter, Logger.Owa, forcedIdentity);
}
public static MfaApiRequestExecutor CreateCrm(HttpContextBase context)
@@ -34,4 +34,4 @@ public static MfaApiRequestExecutor CreateCrm(HttpContextBase context)
return new MfaApiRequestExecutor(context, getter, Logger.IIS);
}
}
-}
\ No newline at end of file
+}
diff --git a/Services/MultiFactorApiClient.cs b/Services/MultiFactorApiClient.cs
index ff00f2f..2810e57 100644
--- a/Services/MultiFactorApiClient.cs
+++ b/Services/MultiFactorApiClient.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Net;
using System.Text;
+using MultiFactor.IIS.Adapter.Core;
namespace MultiFactor.IIS.Adapter.Services
{
@@ -97,6 +98,48 @@ public string CreateRequest(string identity, string rawUserName, string postback
throw new Exception($"{errmsg}", ex);
}
}
+
+ public MultiFactorAccessRequest CreateNonInteractiveAccessRequest(string methodPath, PersonalData personalData)
+ {
+ var url = $"{Configuration.Current.ApiUrl}{methodPath}";
+ var payload = new
+ {
+ Identity = personalData.Identity,
+ Phone = personalData.Phone,
+ Email = personalData.Email
+ };
+
+ var str = Util.JsonSerialize(payload);
+ var requestData = Encoding.UTF8.GetBytes(str);
+ var auth = Convert.ToBase64String(
+ Encoding.ASCII.GetBytes($"{Configuration.Current.ApiKey}:{Configuration.Current.ApiSecret}"));
+ byte[] responseData = null;
+
+ using (var web = new WebClient())
+ {
+ web.Headers.Add("Content-Type", "application/json");
+ web.Headers.Add("Authorization", $"Basic {auth}");
+ web.Headers.Add("mf-trace-id", _getTraceId());
+
+ if (!string.IsNullOrEmpty(Configuration.Current.ApiProxy))
+ {
+ web.Proxy = new WebProxy(Configuration.Current.ApiProxy);
+ }
+
+ responseData = web.UploadData(url, "POST", requestData);
+ }
+
+ var responseJson = Encoding.UTF8.GetString(responseData);
+
+ var response = Util.JsonDeserialize>(responseJson);
+
+ if (!response.Success)
+ {
+ throw new Exception($"Got unsuccessful response from API: {responseJson}");
+ }
+
+ return response.Model;
+ }
}
public class MultiFactorWebResponse
@@ -112,4 +155,14 @@ public class MultiFactorAccessPage
{
public string Url { get; set; }
}
-}
\ No newline at end of file
+
+ public class MultiFactorAccessRequest
+ {
+ public string Id { get; set; }
+ public string Identity { get; set; }
+ public string Status { get; set; }
+ public string Message { get; set; }
+ public bool Granted => Status == "Granted";
+ public bool Denied => Status == "Denied";
+ }
+}
diff --git a/Services/RequestLogBuilder.cs b/Services/RequestLogBuilder.cs
new file mode 100644
index 0000000..6459650
--- /dev/null
+++ b/Services/RequestLogBuilder.cs
@@ -0,0 +1,74 @@
+using System.Collections.Specialized;
+using System.Text;
+using System.Web;
+
+namespace MultiFactor.IIS.Adapter.Services
+{
+ public class RequestLoggerBuilder
+ {
+ public static string BuildRequestLog(HttpContextBase context)
+ {
+ var messageHttpMethod = context.Request.HttpMethod;
+ var requestUrl = context.Request.Url?.AbsoluteUri;
+ var requestParams = context.Request.Params;
+ var form = context.Request.Form;
+ var requestPath = context.Request.Path;
+ var requestQueryString = context.Request.QueryString;
+ var requestHeaders = context.Request.Headers;
+ var appPath = context.Request.ApplicationPath;
+ var identity = context.User?.Identity?.Name;
+ var authType = context.User?.Identity?.AuthenticationType;
+ var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false;
+ var builder = new StringBuilder("Request: ");
+
+ builder.AppendLine($"{nameof(context.Request.HttpMethod)} = {messageHttpMethod}");
+ builder.AppendLine($"{nameof(context.Request.Url.AbsoluteUri)} = {requestUrl}");
+ builder.AppendLine($"{nameof(context.Request.Path)} = {requestPath}");
+ builder.AppendLine($"{nameof(context.Request.QueryString)} = {requestQueryString}");
+ builder.AppendLine($"{nameof(context.Request.ApplicationPath)} = {appPath}");
+
+ builder.AppendLine($"{nameof(context.User.Identity.Name)} = {identity}");
+ builder.AppendLine($"{nameof(context.User.Identity.IsAuthenticated)} = {isAuthenticated}");
+ builder.AppendLine($"{nameof(context.User.Identity.AuthenticationType)} = {authType}");
+
+ string user = context.Request.Params["User"];
+ builder.AppendLine("User = " + user);
+
+ string deviceId = context.Request.Params["DeviceId"];
+ builder.AppendLine("Device Id = " + deviceId);
+
+ string deviceType = context.Request.Params["DeviceType"];
+ string userAgent = context.Request.Params["HTTP_USER_AGENT"];
+ builder.AppendLine("Device Type = " + deviceType);
+ builder.AppendLine("Device UserAgent = " + userAgent);
+
+ var cmd = context.Request.Params["Cmd"];
+ if (!string.IsNullOrWhiteSpace(cmd))
+ {
+ builder.AppendLine("Cmd = " + cmd);
+ }
+
+ builder.AppendLine("--------------------------");
+ builder.AppendLine($"Headers:");
+ AddParams(builder, requestHeaders);
+
+ builder.AppendLine("--------------------------");
+ builder.AppendLine($"Form:");
+ AddParams(builder, form);
+
+ builder.AppendLine("--------------------------");
+ builder.AppendLine($"Parameters:");
+ AddParams(builder, requestParams);
+
+ return builder.ToString();
+ }
+
+ private static void AddParams(StringBuilder builder, NameValueCollection valueCollection)
+ {
+ foreach (var key in valueCollection.AllKeys)
+ {
+ builder.AppendLine($"{key} = {valueCollection[key]}");
+ }
+ }
+ }
+}
diff --git a/WebUtil.cs b/WebUtil.cs
index 86b2497..7d45fdf 100644
--- a/WebUtil.cs
+++ b/WebUtil.cs
@@ -11,6 +11,23 @@ public static bool IsXhrRequest(HttpRequestBase request)
return request.Headers["x-requested-with"] == "XMLHttpRequest";
}
+ ///
+ /// Determines whether the request is the first provisioning request
+ /// https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh531590(v=exchg.140)
+ ///
+ public static bool IsInitialProvisionRequest(HttpRequestBase request)
+ {
+ var cmd = request.Params["Cmd"];
+ var xMsPolicyKey = request.Headers["X-MS-PolicyKey"];
+
+ if (cmd?.ToLower() == "provision" && xMsPolicyKey == "0")
+ {
+ return true;
+ }
+
+ return false;
+ }
+
public static bool IsStaticResourceRequest(Uri uri)
{
var staticContent = new[]
@@ -26,4 +43,4 @@ public static bool IsStaticResourceRequest(Uri uri)
return staticContent.Any(c => path.EndsWith(c));
}
}
-}
\ No newline at end of file
+}