ASP.NET MVC5 Identity account lockout without Entity Framework - entity-framework

I have created an ASP.NET MVC5 application that allows users to access their financial data including invoices, services and products that they have acquired from our company.
The application gets all data from a set of WCF services including a list of all registered users that have access to the system. I'm using ASP.NET Identity object and claims to authorize users into the application, everything works fine I only have to use the credentials (email and password) to invoke the WCF service which returns an object containing the details about the User or a NULL value if there's no match.
However, there's a new requirement to implement an account lockout after 5 failed login attempts (the account will be locked for 20 minutes before allowing users to try again) which is a feature already included in ASP.NET identity 2.0. I have been "googling" for a couple of days, but couldn't find an example (or even a similar approach) of how to implement this requirement without storing users in Entity Framework and a local DB.
Is there any way of adding just the account lockout feature (with the 20 minutes lockout) using a WCF service as a datasource to my ASP.NET MVC5 application? Any ideas?
This is actually my first ASP.NET MVC5 application, so don't really know much about all features provided on it, any help will be appreciated.
This is how the Login (POST) looks like:
//Authentication handler
IAuthenticationManager Authentication
{
get { return HttpContext.GetOwinContext().Authentication; }
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
try
{
//Validate user from service
CWP_AccountService.User userObject = AccountClient.GetUserByCredentials(model.Email, model.Password);
if (userObject != null)
{
//Create Claims
var identity = new ClaimsIdentity(
new[] { new Claim(ClaimTypes.Name, userObject.Email) },
DefaultAuthenticationTypes.ApplicationCookie,
ClaimTypes.Name, ClaimTypes.Role);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userObject.Email));
identity.AddClaim(new Claim(ClaimTypes.Sid, userObject.UserID.ToString()));
int cookieDurationPersistence = 20160;
int cookieDuration = 2;
Authentication.SignIn(new AuthenticationProperties { IsPersistent = model.RememberMe, ExpiresUtc = (model.RememberMe) ? DateTime.Now.AddMinutes(cookieDurationPersistence) : DateTime.Now.AddMinutes(cookieDuration) }, identity);
//Check if there is a local return URL
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/") && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return RedirectToLocal(returnUrl);
}
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("system-error", "The Email/Password provided is not valid.");
}
}
catch(Exception ex)
{
ModelState.AddModelError("system-error", "Error");
logger.Warn(ex.ToString());
}
}
return View(model);
}
The Startup.Auth.cs:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
}

Related

How to authenticate with Azure AD / OpenId but use Entity Framework based user/role data

I'm trying to improve the authentication story for a legacy ASPNet MVC/OWIN app - Currently, it uses the AspNetUsers / AspNetRoles / claims etc tables along with forms + cookie based authentication.
I want to use Azure AD / OpenID Connect for authentication but then load the user profile/roles from the database as currently. Basically, no more password management within the app. Users themselves will still need to exist/be created within the app.
The application is quite dependent on some custom data associated with these users so simply using the roles from Active Directory isn't an option.
The OpenID auth works, however I'm not sure how to use the existing Identityuser / IdentityUserRole / RoleManager plumbing in conjunction with it.
Basically once the user authenticates with Open ID we'll want to load the corresponding user from the database (matching on email address) and use that user profile / roles going forward.
In particular, the AuthorizeAttribute (with specific roles specified) should continue to function as before.
This is what I have so far:
public class IdentityConfig
{
public void Configuration(IAppBuilder app)
{
app.CreatePerOwinContext(AppIdentityDbContext.Create);
app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
ConfigureAuth(app);
}
/// <summary>
/// Configures OpenIDConnect Authentication & Adds Custom Application Authorization Logic on User Login.
/// </summary>
/// <param name="app">The application represented by a <see cref="IAppBuilder"/> object.</param>
private void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
//Configure OpenIDConnect, register callbacks for OpenIDConnect Notifications
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigHelper.ClientId,
Authority = String.Format(CultureInfo.InvariantCulture, ConfigHelper.AadInstance,
ConfigHelper.Tenant), // For Single-Tenant
PostLogoutRedirectUri = ConfigHelper.PostLogoutRedirectUri,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
RoleClaimType = "roles",
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error/OtherError?errorDescription=" +
context.Exception.Message);
return Task.FromResult(0);
},
SecurityTokenValidated = async context =>
{
string userIdentityName = context.AuthenticationTicket.Identity.Name;
var userManager = context.OwinContext.GetUserManager<AppUserManager>();
var user = userManager.FindByEmail(userIdentityName);
if (user == null)
{
Log.Error("User {name} authenticated with open ID, but unable to find matching user in store", userIdentityName);
context.HandleResponse();
context.Response.Redirect("/Error/NoAccess?identity=" + userIdentityName);
return;
}
user.DateLastLogin = DateTime.Now;
IdentityResult result = await userManager.UpdateAsync(user);
if (result.Succeeded)
{
var authManager = context.OwinContext.Authentication;
ClaimsIdentity ident = await userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ExternalBearer);
// Attach additional claims from DB user
authManager.User.AddIdentity(ident);
// authManager.SignOut();
// authManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);
return;
}
throw new Exception(string.Format("Failed to update user {0} after log-in", userIdentityName));
}
}
});
}
}
Here's what I ended up doing:
public class IdentityConfig
{
public void Configuration(IAppBuilder app)
{
app.CreatePerOwinContext(AppIdentityDbContext.Create);
app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
ConfigureAuth(app);
}
/// <summary>
/// Configures OpenIDConnect Authentication & Adds Custom Application Authorization Logic on User Login.
/// </summary>
/// <param name="app">The application represented by a <see cref="IAppBuilder"/> object.</param>
private void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieDomain = ConfigHelper.AuthCookieDomain,
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromHours(2)
});
//Configure OpenIDConnect, register callbacks for OpenIDConnect Notifications
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigHelper.ClientId,
Authority = String.Format(CultureInfo.InvariantCulture, ConfigHelper.AadInstance, ConfigHelper.Tenant),
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
RoleClaimType = ClaimTypes.Role
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error/OtherError?errorDescription=" + context.Exception.Message);
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
// Set the post-logout & redirect URI dynamically depending on the incoming request.
// That allows us to use the same Azure AD app for two subdomains (these two domains give different app behaviour)
var builder = new UriBuilder(context.Request.Uri);
builder.Fragment = builder.Path = builder.Query = "";
context.ProtocolMessage.PostLogoutRedirectUri = builder.ToString();
context.ProtocolMessage.RedirectUri = builder.ToString();
return Task.FromResult(0);
}
}
});
app.Use<EnrichIdentityWithAppUserClaims>();
}
}
public class EnrichIdentityWithAppUserClaims : OwinMiddleware
{
public EnrichIdentityWithAppUserClaims(OwinMiddleware next) : base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await MaybeEnrichIdentity(context);
await Next.Invoke(context);
}
private async Task MaybeEnrichIdentity(IOwinContext context)
{
ClaimsIdentity openIdUserIdentity = (ClaimsIdentity)context.Authentication.User.Identity;
string userIdentityName = openIdUserIdentity.Name;
var userManager = context.GetUserManager<AppUserManager>();
var appUser = userManager.FindByEmail(userIdentityName);
if (appUser == null)
{
Log.Error("User {name} authenticated with open ID, but unable to find matching user in store", userIdentityName);
return;
}
appUser.DateLastLogin = DateTime.Now;
IdentityResult result = await userManager.UpdateAsync(appUser);
if (result.Succeeded)
{
ClaimsIdentity appUserIdentity = await userManager.CreateIdentityAsync(appUser, DefaultAuthenticationTypes.ExternalBearer);
openIdUserIdentity.AddClaims(appUserIdentity.Claims);
}
}
}
It's quite similar to what I had originally - (note: RoleClaimType = ClaimTypesRoles not "roles") except instead of trying to deal with the user in the SecurityTokenValidated callback, I've added some custom middleware that finds a matching user (by email address) and adds the claims (app roles) from the matching app user to the authenticated user identity (OpenID identity).
Finally, I protected all controller actions with a (custom) AuthorizeAttribute (not shown here) that ensures the authenticated user at least belongs to the "User" role (if not, redirects them to a "no access" page indicating that we've authenticated them but they have no access in the system).
The OpenID auth works, however I'm not sure how to use the existing Identityuser / IdentityUserRole / RoleManager plumbing in conjunction with it.
The application is quite dependent on some custom data associated with these users so simply using the roles from Active Directory isn't an option.
For your requirement, I assume that you could build your identity server (e.g. IdentityServer3) and leverage IdentityServer3.AspNetIdentity for identity management using ASP.NET Identity.
For your web client application, you could use OpenID Connect middleware and set Authority to your custom identity server and set your pre-configured ClientId on your identity server.
Moreover, you could follow this tutorial for quickly getting started with IdentityServer3 and the full samples IdentityServer3 Samples.

Claims transformation in ADFS 3.0 by making a call to REST API

We have a ASP.NET Web API (REST service) in our enterprise that gives us the list of coarse-grained claims for a user that we want to inject into the adfs token before passing the token onto the application. Does anyone know if making a rest call is possible using the Custom attribute store (by passing param's to the custom attribute store from the Claims rule language in ADFS 3.0) ?
Any help regarding this would be greatly appreciated!
Thanks,
Ady.
I'm able to make the REST call from the Custom Attribute store. For those who are still wondering about this can look at the below code.
using System;
using System.Collections.Generic;
using System.Text;
using System.IdentityModel;
using Microsoft.IdentityServer.ClaimsPolicy.Engine.AttributeStore;
using System.Net.Http;
using System.Net;
namespace CustomAttributeStores
{
public class CoarseGrainClaimsAttributeStore : IAttributeStore
{
#region Private Members
private string API_Server = "https://<Server Name>/API/";
#endregion
#region IAttributeStore Members
public IAsyncResult BeginExecuteQuery(string query, string[] parameters, AsyncCallback callback, object state)
{
string result = string.Empty;
if (parameters == null)
{
throw new AttributeStoreQueryFormatException("No query parameter.");
}
if (parameters.Length != 1)
{
throw new AttributeStoreQueryFormatException("More than one query parameter.");
}
string userName = parameters[0];
if (userName == null)
{
throw new AttributeStoreQueryFormatException("Query parameter cannot be null.");
}
//Ignore SSL Cert Error
//TODO: Need to set the SSL cert correctly for PROD Deployment
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient())
{
//The url can be passed as a query
string serviceUrl = API_Server + "GetAdditionalClaim";
serviceUrl += "?userName=" + userName;
//Get the SAML token from the API
result = client
.GetAsync(serviceUrl)
.Result
.Content.ReadAsStringAsync().Result;
result = result.Replace("\"", "");
}
string[][] outputValues = new string[1][];
outputValues[0] = new string[1];
outputValues[0][0] = result;
TypedAsyncResult<string[][]> asyncResult = new TypedAsyncResult<string[][]>(callback, state);
asyncResult.Complete(outputValues, true);
return asyncResult;
}
public string[][] EndExecuteQuery(IAsyncResult result)
{
return TypedAsyncResult<string[][]>.End(result);
}
public void Initialize(Dictionary<string, string> config)
{
// No initialization is required for this store.
}
#endregion
}
}
No, you can't do this with the claims rules language.
This is somewhat of a hack but you could write the claims to e.g. a DB and then use a custom attribute store
If you have access to WIF on the client side, you can augment the claims there e.g. Adding custom roles to windows roles in ASP.NET using claims.

Share User data between different action in the same controller in MVC

I want to store globally the object User (that is the table USER in my db) in my HomeController, in that way i don't have to instantiate it in every single action.
I found the following solution that works pretty fine
public class HomeController : Controller
{
private DatabaseContext db = new DatabaseContext();
private User currentUser;
private User CurrentUser
{
get
{
if (User.Identity.IsAuthenticated)
//This function returns the object "User" (table USER in db) based on the PK of the table
currentUser = CustomDbFunctions.GetUserEntityFromUsername(User.Identity.Name, db);
return currentUser;
}
}
public ActionResult Index()
{
if (User.Identity.IsAuthenticated)
return View(CurrentUser);
else
return Redirect("login");
}
}
I'd like to know if there's a better (or more elegant) way to achieve the same goal.
Please note that i'm not using the MembershipProvider.
In your example the user object is instantiated in every single action (in contrast to what you said). This is because actions are usually invoked per http request and controller instances are disposed after each use.
Your code shares the instance structurally (you don't have to repeat the code) which is ok but what about sharing the code between different controllers? I'd suggest to refactor your GetUserEntityFromUsername a little bit so that you retrieve the object only once per request, using the Items container to get the request scope:
public class CustomDbFunctions
{
const string itemsUserKey = "_itemsUserKey";
public static User GetUserEntityFromUsername( IPrincipal principal, DatabaseContext db )
{
if ( principal == null || principal.Identity == null ||
!principal.Identity.IsAuthenticated
)
return null;
if ( HttpContext.Current.Items[itemsUserKey] == null )
{
// retrieve the data from the Db
var user = db.Users.FirstOrDefault( u => u.Name == User.Identity.Name );
HttpContext.Current.Items[itemsUserKey] = user;
}
return (User)HttpContext.Current.Items[itemsUserKey];
}
This way your wrapper takes care of retrieving the instance from the database once per request.
Note that this requires sharing your database context as entities should not be reused on different contexts. Fortunately, this can be done in a similar way:
public class CustomDbFunctions
{
const string dbUserKey = "dbUserKey";
public static DatabaseContext CurrentDatabaseContext
{
get
{
if ( HttpContext.Current.Items[dbUserKey] == null )
{
DatabaseContext ctx = new DatabaseContext(); // or any other way to create instance
HttpContext.Current.Items[dbUserKey] = ctx;
}
return (DatabaseContext)HttpContext.Current.Items[dbUserKey];
}
}
This way, the context instance, shared per request, is always available as
CustomDbFunctions.CurrentDatabaseContext

Setting realm and issuer for web service at Runtime (windows azure ACS)

We have the scenario in our project,
We have Tenant-1 to Tenant-n which consume Restful Service S1. The tenants have a 1 to 1 relationship with IDP. Client has to federate the tenant UI through Restful Service using ACS with the help of tenant specific IDP configured in ACS at the time of onboarding.
Tenant-1 mapped to IdP1 (Eg: Yahoo)
Tenant-2 mapped to Idp2 (Eg: Google)
Restful Service returns a JavaScript as JSON, which is hosted within the Tenant’s Web UI. So if the tenant has already logged on to the Tenant UI using IDP specific to him via his own application, then for any requests from the tenant UI to Restful Service, the Restful service should federate to tenant specific IdP based on the partner information (mapping of tenant to IdP) configured during the onboarding process.
I am setting Realm in the Global.asax as shown below.
public class WebApiApplication : System.Web.HttpApplication
{
public event EventHandler RedirectingToIdentityProvider;
public override void Init()
{
FederatedAuthentication.WSFederationAuthenticationModule.RedirectingToIdentityProvider += WSFederationAuthenticationModule_RedirectingToIdentityProvider;
}
void WSFederationAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
{
Tenant tenant = GetTenantDetails(subId); // Gets the tenant information from MetaData based on subscriptionId
if (tenant != null)
{
e.SignInRequestMessage.Realm = tenant.Realm + "CMS/";
}
}
protected void Application_Start()
{
FederatedAuthentication.FederationConfigurationCreated += OnServiceConfigurationCreated;
}
private void OnServiceConfigurationCreated(object sender, FederationConfigurationCreatedEventArgs e)
{
if (tenant != null)
{
e.FederationConfiguration.WsFederationConfiguration.Issuer = tenant.Issuer;
Uri uri = new Uri(tenant.Realm + "CMS/");
if (!e.FederationConfiguration.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Contains(uri))
e.FederationConfiguration.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(tenant.Realm + "CMS/"));
e.FederationConfiguration.WsFederationConfiguration.Realm = tenant.Realm + "CMS/";
}
}
Further the Realm is set at the per request level too, as shown below.
public class MetaDataModule : IHttpModule
{
private static string WSFederationAuthenticationModuleName = string.Empty;
public void Init(HttpApplication httpContextApplication)
{
var requestWrapper = new EventHandler(DoSyncRequestWorkToGetTenantDetails);
httpContextApplication.BeginRequest += requestWrapper;
}
private static void DoSyncRequestWorkToGetTenantDetails(object sender, EventArgs e)
{
var httpContextApplication = (HttpApplication)sender;
Tenant tenant = GetTenantDetails(); // Gets the tenant information from MetaData based on subscriptionId
if (tenant != null)
{
WSFederationAuthenticationModule wsfed = (WSFederationAuthenticationModule)httpContextApplication.Modules["WSFederationAuthenticationModule"];
wsfed.FederationConfiguration.WsFederationConfiguration.Issuer = tenant.Issuer;
Uri uri = new Uri(tenant.Realm + "CMS/");
if (!wsfed.FederationConfiguration.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Contains(uri))
wsfed.FederationConfiguration.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(tenant.Realm + "CMS/"));
wsfed.FederationConfiguration.WsFederationConfiguration.Realm = tenant.Realm + "CMS/";
//FederatedAuthentication.FederationConfiguration.WsFederationConfiguration.Issuer = tenant.Issuer;
//Uri uri = new Uri(tenant.Realm + "CMS/");
//if (!FederatedAuthentication.FederationConfiguration.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Contains(uri))
// FederatedAuthentication.FederationConfiguration.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(tenant.Realm + "CMS/"));
//FederatedAuthentication.FederationConfiguration.WsFederationConfiguration.Realm = tenant.Realm + "CMS/";
}
}
Please find the modules registered in the Web.config and the remaining part of WIF configuration too.
In spite of resetting the Realm for each request, the new value does not get assigned.
Client does not want their tenants to implement any authentication or federation related code from their end for this to work.
Please let me know if you can think of any solution to this issue with the help of Passive Federation.
You should customize the realm in the Application_AuthenticateRequest method in your Global.asax.
Take a look at this link.

ActionFilterAttribute to turn off SSL on Asp.Net MVC2 controller doesn't work consistently

This Action Filter doesn't seem to work consistently. Some times it turns SSL off and sometimes it doesn't. I have it applied to the entire controller at it's declaration.
public class SSLFilter:ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpRequestBase req = filterContext.HttpContext.Request;
HttpResponseBase res = filterContext.HttpContext.Response;
if (req.IsSecureConnection)
{
var builder = new UriBuilder(req.Url)
{
Scheme = Uri.UriSchemeHttp,
Port = 80
};
res.Redirect(builder.Uri.ToString());
}
base.OnActionExecuting(filterContext);
}
}
It's kind of odd...any ideas why it might be working sporadically?
Have you tried decorating your controllers/actions with the [RequireHttps] attribute?
Oops, haven't noticed you was asking about ASP.NET MVC 2. This attribute is available in ASP.NET MVC 3 only, so here's the source code for it (as implemented in ASP.NET MVC 3):
using System;
using System.Diagnostics.CodeAnalysis;
using System.Web.Mvc.Resources;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class RequireHttpsAttribute : FilterAttribute, IAuthorizationFilter {
public virtual void OnAuthorization(AuthorizationContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (!filterContext.HttpContext.Request.IsSecureConnection) {
HandleNonHttpsRequest(filterContext);
}
}
protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext) {
// only redirect for GET requests, otherwise the browser might not propagate the verb and request
// body correctly.
if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) {
throw new InvalidOperationException(MvcResources.RequireHttpsAttribute_MustUseSsl);
}
// redirect to HTTPS version of page
string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}
Notice that how instead of doing any redirects it uses a RedirectResult which is the correct way of performing redirects in ASP.NET MVC => by returning action results:
filterContext.Result = new RedirectResult(url);
Not only that this will perform the correct redirect but that's how to short-circuit the execution of an action. Also semantically your filter should actually be an IAuthorizationFilter as you are blocking access to some resource here.