Sunday, May 15, 2016

Microsoft 70-486: Configure authentication

Exam Objectives


Authenticate users; enforce authentication settings; choose between Windows, Forms, and custom authentication; manage user session by using cookies; configure membership providers; create custom membership providers; configure ASP.NET Identity

Quick Overview of Training Materials


How to configure IIS client certificate mapping authentication for IIS7
MSDN - How to: Implement a Custom Membership User



This objective and objective 5.2 (Authorization) are generally discussed together in the books, so dividing it between two posts is a bit artificial.

This post will focus on authentication, which is the process of verifying that a user of your application is who they say they are.


Authenticate Users


The Exam Ref does a pretty thorough job of touching on all the various options for authentication.  I'd only ever dealt with Forms and Windows authentication in a .NET application, so it was an eye opener that so many options were available.  Most of the other authentication mechanisms are challenge-response based and handled through IIS:


Challenge Based (IIS handles authentication):

Basic: 
Username and password are passed in Authorization header as base64 string. Simple, but potentially vulnerable because password is sent in plain text. Uses AD or local server users. Basic access authentication (wikipedia) Basic Authentication using Custom ActionFilter

While I had seen Basic authentication before in the url form (user:password@domain.com...), I didn't realize that the Authorization header was populated the way it is.  Below are screen shots of the browser and server sides of the interaction.  While I was able to get IIS Express doing it by monkeying with the config file, it never really worked very well.   Probably more needs to happen in the app itself to really use Basic authentication.




Digest: 
Unlike Basic authentication, credentials are not sent clear text but instead as an MD5 hash. Uses AD or local server users. Digest access authentication (wikipedia) Digest Authentication configuration (IIS) How Digest Authentication Works

Windows: 
(Also referred to as “Integrated”) Logs in using Windows account, with NTLM or Kerberos. In IIS express configuration, the Negotiate provider will use Kerberos if available. Also possible for application to impersonate the logged in user through a configuration change (ASP.NET Impersonation) Windows Authentication Providers



Client Certificate Mapping: 
A certificate handshake basically. Much more complicated than other forms of authentication. The “Client” variety relies on Active Directory, whereas the “IIS” variety will check the local user store on the server. Client Certificate Mapping Config (IIS) How it works on the wire: IIS HTTP Client certificate authentication


Login Redirection Based (Application handles authentication):

Forms authentication: 
IIS and Windows do not interfere with authentication, instead it is done strictly through the application. I'll cover the details in a bit... How to Implement Forms Based Authentication

Claims based authentication: 
I cover this in depth in my Federation post.


Enforce Authentication Settings


This to me seems like a grey area between authentication and authorization (leaning on the authorization side of the fence really...), but Microsoft included it here so, there you have it.  Also, this section of the Exam Ref read an awful lot like the Professional MVC 5 book (a previous edition covering MVC 4 had the same chapter... hmmm...)

To ensure a user is authenticated before they access a resource, you can decorate that resource with the [Authorize] attribute.  When I say "resource" I'm talking controllers or their action methods.  You can also set the authorize attribute globally, which will protect the entire application.  Of course, if you are using Forms authentication, you will need to allow access to the registration and login methods on the Accounts controller.  This can be done using the [AllowAnonymous] attribute.  [Authorize] is also used for more fine grained access control, but I'll cover that in the authorization post.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
 
        //Add this line for global [Authorize]
        filters.Add(new AuthorizeAttribute());  
    }
}

One thing to note about using the global filter, is that it is only global to MVC.  Web forms, static content, and other handlers are not covered.  Also,  URL Authorization (using <location> and <authorization> elements in web.config) is not reliable in MVC because multiple paths may map to the same controller action (thought this could be a valid way to secure static content and handlers outside of MVC...).  Just something to be mindful of.

It is also possible to check for an authenticated user programmatically, by calling IsAuthenticated on the current user identity property.  This is stored in HttpContext.User.Identity or System.Threading.Thread.CurrentPrincipal.Identity.  In the following screenshot you can see that the Principal stored in both locations is a RolePrincipal, and the Identity is a FormsIdentity:


According to the WROX book, it is the IsAuthenticated method that the framework uses to determine if a user is logged in.


Principals and Identities


I think its worth taking a slight detour to talk about the Principal and Identity objects.  The MSDN article "Principal and Identity Objects" explains exactly what these objects do and the concepts they represent.  Basically, an Identity encapsulates information about the user, while a Principal bundles an identity together with their roles.  IPrincipal and IIdentity are the respective interfaces for these objects, and are implemented by a number of classes in the framework.

Before .NET 4.5, the various Principal and Identity classes inherited from Object. As of 4.5 they all inherit from ClaimsPrincipal and ClaimsIdentity, and as a result all will be aware of claims.  The FormsIdentity, for example, will contain claims for a user logged in using Forms authentication:


WindowsIdentity (which is what you'll have there if you use Windows authentication) contains many more properties and claims:



Implementations of IPrincipal:

Implementations of IIdentity:

Don’t confuse Microsoft.IdentityModel.Claims namespace with System.IdentityModel.Claims namespace, which is used by WCF prior to .NET 4.5. Both namespaces were integrated into System.Security.Claims in 4.5.

WindowsPrincipal and WindowsIdentity are used by AD and other Windows user stores (though by the time it gets to HttpContext, the WindowsPrincipal has been replaced with a RolePrincipal... which actually is a sign that the Membership system is still trying to do it's thing, which is a problem if you are trying to use the new Identity system, see this StackOverflow question). FormsIdentity is used by Forms authentication. Everything else apparently uses the generic classes.

When role management is enabled, the RoleManagerModule assigns a RolePrinciple to the HttpContext.User, which overwrites whatever Principle was already there (Generic with forms, Windows with Windows auth).  Details are on this dinosaur page..


Windows, Forms, or Custom...


Choosing the appropriate authentication mechanism will depend on the environment and user requirements for the application.

Windows authentication is common on intranet and extranet applications. The login is handled by the browser, Windows, and IIS, rather than application code. Windows token is sent with HTTP request, the application checks the domain or local machine for account, determines users assigned groups which translate into roles. Windows authentication must be configured in IIS, and enabled in the application through configuration.


The author keeps insisting that Windows authentication requires the use of Internet Explorer, but I've had Chrome apparently work on my local machine (although to be fair, the whole website basically broke when I switched to Windows Auth...).  Some Googling around seemed to indicate that there can be issues with Chrome and Kerberos, which seems to require a lot of configuration hacking to get working.  It seems the issue is with integrated Windows authentication, in which the browser grabs your Kerberos ticket without prompting you for credentials.  It can be done, but it requires some extra steps.

Forms Authentication puts the onus of managing user authentication on the application itself.  IIS is set to "Anonymous" authentication, because all requests are passed through to the application.  User authentication information is either stored in an encrypted cookie, or stored on the server with an identifier that is included on each request (either through a reference cookie or in the URL for cookieless sessions).  If this cookie information is not present, the user is not authenticated and the login process is initiated with a redirect (HTTP 302) to the configured login page.  Configuring Forms authentication in .NET 4 can be pretty minimal:

    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login" timeout="2880" />
    </authentication>

In 4.5, this has changed to be managed by Windows Identity and OWIN apparently.  OWIN still has Forms authentication, but no longer supports cookieless authentication.  A blog post on MSDN explains the differences.  Classic Forms Authentication is an HTTPModule, which is why on an OWIN setup you'll see a web.config entry removing the Forms Auth module:

  <system.webServer>
    <modules>
      <remove name="FormsAuthentication" />
    </modules>
  </system.webServer>

One important security precaution to take is that the login page should always be served using SSL (HTTPS) to prevent various security threads (man in the middle, packet sniffing, injection... yeah, it's very, very bad not to do this...).  The Don't be That Guy article does a good job of walking through the flow of Forms Authentication and touches on the security implications of unsecured login pages.


Manage Session in Cookies


I'm not going to spend a lot of time on cookies, since I did cover it some in my post on state management.  However, I will quickly cover the cookie part of two authentication options that utilize them:  The Forms Ticket cookie, and the Federated Authentication cookie.

When a user authenticates with Forms authentication, the application creates a ticket.  This ticket is then stored in a cookie, which is passed to the server on each subsequent request (unless cookies are turned off, in which case a reference it passed to the server so it can look up the authentication data).  This ticket is available as part of the FormsIdentity, from either the HttpContext User or the thread current principal.  The FormsIdentity class has a "UserData" property that can store additional data about the user in the cookie.

I found a blog post that outlines how to leverage the UserData property.  His example uses an MVC 3 application it looks like, but I was able to replicate the results in MVC4 and I believe MVC5 would follow a similar pattern.  Below you can see before and after screenshots:

Before. Notice UserData is empty

After. UserData has our JSON string in it
There are drawbacks to this approach, which interesting enough Microsoft points out on the MSDN page for the class.  The UserData property is read only, and can only be set through a constructor parameter.  Also, if too much data is shoved in there, it could result in an invalid cookie.

For what it's worth, if you need extra properties for a user, it can be done by extending the Entity Framework model for the user.  This answer on Stack Overflow shows how simple it is to accomplish in MVC 5.  In MVC 5 you can add custom properties to the "ApplicationUsers" class, and in MVC 4 there is a "UserProfile" class that can be similarly extended.

That Stack Overflow answer segues nicely into the other way that user state can be persisted in cookies, and that is by storing extra claims in the ClaimsIdentity.  Since Claims data is stored in the FedAuth cookie(s), this is effectively storing user data in the cookie.  Unlike the problem with UserData overloading the cookie, the FedAuth cookie is chunked to keep the size of each individual cookie under the 4k limit, though performance could suffer if the cookies aggregate size got excessive.  Below is an example of how claims might be added to a ClaimsIdentity at runtime:

public ActionResult Index()
{
 
    var fakeClaim = "d7 fd d1 53 ... 6c 7a d9 7f 19 9a e5 3f ";
 
    var cp = ClaimsPrincipal.Current;
    var ci = cp.Identity as ClaimsIdentity;
    ci.AddClaim(new Claim(
            "MadeUpType",
            fakeClaim + fakeClaim + fakeClaim + fakeClaim + fakeClaim
        ));
 
    var sam = FederatedAuthentication.SessionAuthenticationModule;
    var currentToken = sam.ContextSessionSecurityToken;
    var from = currentToken.KeyEffectiveTime;
    var to = currentToken.KeyExpirationTime;
    var newToken = sam.CreateSessionSecurityToken(
        new ClaimsPrincipal(ci),
        "",
        from,
        to,
        true
    );
 
    sam.WriteSessionTokenToCookie(newToken);
 
    return View();
}

The fake data was a 1000 bytes of random data.  These shots show the impact on the size of the FedAuth cookies:

Before, with default claims

After, with all the padding

See my posts on State Management and Distributed Applications for more on session management scenarios.


Configure and Create Membership Providers


So before I can really discuss how to configure and create membership providers, it's necessary to go over some of history.  My sense for this history mostly comes from Jon Galloways post on SimpleMembership (which, incidentally, is yet another source shamelessly plagiarized by the Exam Ref... smh).

ASP.NET 2.0 introduced the Membership system in 2005.  This remained the way membership was done through MVC4.  In fact, if you create a new MVC3 application in VS 2012, it will use all the classic membership system plumbing.  MVC4 introduced SimpleMembership, which was meant to be easier to use and address shortcomings in the existing membership system, which was built around a number of assumptions (username/password auth, SQL Server) that were difficult to work with in other scenarios (some other data base, OAuth, OpenId).  So the MVC4 templates changed to replace the old membership system with SimpleMembership.

Of course, then you have MVC5 come along, and SimpleMembership has been replaced with ASP.NET Identity.  The Intro to ASP.NET Identity article covers some of the shortcomings of both the traditional membership system and SimpleMembership.  SimpleMembership doesn't work well with the existing providers, and neither works with OWIN.  The article also touches on Universal Providers, which address some of the issues with working with databases other than SQL Server but are still based on MembershipProvider and still share many of the same limitations.

So I'll discuss customizing and creating MembershipProvider based systems in this section, and ASP.NET Identity in the last section.  Here are the relevant Membership Provider classes:

ASP.NET 2.0:
System.Web.Security.MembershipProvider: Abstract base class implemented by all the other Membership providers


Nuget Package circa 2011 (Universal Provider):

MVC4 added (SimpleMembership):
WebMatrix.WebData.ExtendedMembershipProvider
WebMatrix.WebData.SimpleMembershipProvider (Use in conjunction with WebMatrix.WebData.WebSecurity class to handle registration, login, password management functionality.  Default in MVC4 template)


Configure and Create MembershipProvider


Based on the information the How To Use Membership in ASP.NET 2.0 and Sample Implementation MSDN articles, I created an in-memory membership provider.  While this obviously wouldn't be good for anything except maybe testing, it was a good exercise for learning the basics of implementing a membership provider.  Configuration is easy enough, as membership providers are simply configured using attributes on a web.config element:

  <system.web>
    <membership defaultProvider="InMemoryProvider">
      <providers>
        <add name="InMemoryProvider" 
             type="CustomMembershipProvider.Provider.InMemoryProvider, CustomMembershipProvider"
             minRequiredPasswordLength="6" 
             minRequiredNonalphanumericCharacters="0" 
             requiresQuestionAndAnswer="false" 
             maxInvalidPasswordAttempts="3" 
             passwordAttemptWindow="15"
             />
      </providers>
    </membership>
  </system.web>

One "gotcha" I ran into early (before finding the sample implementation) dealt with configuration.  When your custom class inherits from MembershipProvider, you have to implement all the abstract class methods, but you don't automatically override the Initialize method from the Provider base class that MembershipProvider inherits.  Failing to override this class results in pretty wonky behavior, particularly with regards to the database.  If you don't provide a valid connection string, the application crashes.  By overriding this class, I could avoid the connection string completely and just concentrate on the MembershipProvider API.

Because it was all in-memory, writing unit tests was easy, and they ran lightning fast.  Here is an example test (along with the sut creation helper):

[Test]
public void GetUsernameByEmail_UserExists_ReturnsUsername()
{
    //Arrange
    var sut = getSut();
 
    //Act
    var result1 = sut.GetUserNameByEmail("email");
 
    //Assert
    Assert.AreEqual("troy", result1);
}
 
private InMemoryProvider getSut()
{
    var sut = new InMemoryProvider();
    var section = (MembershipSection)WebConfigurationManager
        .GetSection("system.web/membership");
    var config = new NameValueCollection();
 
    sut.Initialize("InMemoryProvider", 
        section.Providers["InMemoryProvider"].Parameters);
    MembershipCreateStatus status;
    sut.CreateUser("troy""password""email",
        nullnulltrue"troy"out status);
 
    return sut;
}

MembershipProvider has a pretty big interface.  Many of the getters are set from the config file (those that aren't get a default value).

Methods:

  • bool ChangePassword
  • bool ChangePasswordQuestionAndAnswer
  • MembershipUser CreateUser
  • bool DeleteUser
  • MembershipUserCollection FindUsersByEmail
  • MembershipUserCollection FindUsersByName
  • MembershipUserCollection GetAllUsers
  • int GetNumberOfUsersOnline
  • string GetPassword - by answer
  • MembershipUser GetUser - by string username 
  • MembershipUser GetUser - by object providerUserKey
  • string GetUserNameByEmail
  • string ResetPassword - by answer
  • bool UnlockUser
  • void UpdateUser
  • bool ValidateUser
Getters
  • string ApplicationName
  • bool EnablePasswordReset
  • bool EnablePasswordRetrieval
  • int MaxInvalidPasswordAttempts
  • int MinRequiredNonAlphanumericCharacters
  • int MinRequiredPasswordLength
  • int PasswordAttemptWindow
  • MembershipPasswordFormat PasswordFormat
  • string PasswordStrengthRegularExpression
  • bool RequiresQuestionAndAnswer
  • bool RequiresUniqueEmail

In my simple provider I basically ignore the configuration setting within the provider, in a real provider you'd probably want to do some validation based on the settings.  The other thing that tripped me up was storing the password and passwordAnswer.  These are relied on by several methods, but the MembershipUser class doesn't have properties for them.  As a result, I had to subclass MembershipUser and add properties for the password and passwordAnswer.  I'm pretty sure it's by design, as the intent is for these to be hashed and stored in the database and not kept as a properties on the user object.

The complete code for the in-memory provider can be found in my Github repo.


Configure and Create SimpleMembership


My first attempt at creating some customization with SimpleMembership involved follow this tutorial on creating a subclass of the SimpleMembership provider.  He overrides the ValidateUser method, and writes code to look up a user based on a password hash.  It's a non-tivial example, and I ultimately couldn't get it to work quite right, probably because I tried to slip it into an existing project template.  As a result I kept getting the old and new tables stepping on each other, or various code wasn't pulling from the right table... anyway, it was a mess.  If I'd have built something up from scratch, as described in this older article, I probably could have made it work.  As it was, I decided to do something simpler rather than thrash.

I figured it would be instructive to just add a couple additional properties to the UserProfile model and use them in the view somewhere.  As Jon Galloway tells it in his article on SimpleMembership, it's supposed to be super easy to throw additional attributes on the UserProfile model and just go to town, but I didn't find it quite so simple.  This may be owing to my inexperience with Entity Framework (EF) but the answers I found on Stack Overflow addressing the problems I kept running into seemed to indicate it isn't just me.

SimpleMembership is a bit of a misnomer.  It may be simpler to implement, out of the box, than a straight MembershipProvider, but creating an In-memory equivalent to the MembershipProvider I created above would be quite onerous (If it was even possible...).  Below are side-by-side views of the databases and objects created for each type of provider:



The MVC4 Template comes with an initializer for the database, which is implemented in a filter and applied to the AccountController, so anytime someone logs in, registers, or doesn't anything else with the authentication system, it runs the init code.  The line that actually initializes the database is this:

WebSecurity.InitializeDatabaseConnection("DefaultConnection", 
    "UserProfile""UserId""UserName", autoCreateTables: true);

By specifying the connection string, the table with the user data ("UserProfile") and the columns for the unique identities and user name ("UserId" and "UserName"), WebSecurity will set up simple membership to use those for the user data.  This gives you a certain amount of flexibility out of the box, since you can specify an existing database and use the schema already present.  You are still limited to flavors of SQL Server it seems (according to the Galloway article).  One strategy that seems to work for other database types is to implement the ExtendedMembershipProvider class to utilize the desired store (StackOverflow question, and a MongoDb implementaion).

So, the demo code I created is on Github, there really wasn't much to it.  Just mostly followed the instruction I found on Stack Overflow plus a bit of tweaking with the views to take advantage of the added properties.  Some of the SO code seemed to choke, but it could have been developer error.  One thing that wasn't immediately obvious was how to save the additional properties when the user is created.  By default, the AccountController calls the WebSecurity.CreateUserAndAccount() method that just takes a UserName and Password.  There is an overload, however, that takes an object with the additional properties, like so:

WebSecurity.CreateUserAndAccount(model.UserName, model.Password, 
    new { FavColor = model.FavColor, CatsName = model.CatsName }, false);

Later, I was able to pull these values from the database and throw them in the ViewBag to display on the page:

using (UsersContext db = new UsersContext())
{
    var username = HttpContext.User.Identity.Name;
    UserProfile user = db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == username.ToLower());
    ViewBag.FavColor = user.FavColor;
    ViewBag.CatsName = user.CatsName;
}
 
return View();



Configure and Create Universal Provider


Scott Hanselman wrote a post on his blog in 2011 announcing the alpha release of the UniversalProviders, which were used for not just Membership, but Roles, Profiles, and Session as well.  The Visual Studio 2012 MVC3 templates includes them by default.  Jon Galloway also discusses them in his post on SimpleMembership (you know, the one where he touts it as the future... luckily he came to his senses).

The Universal Providers are build on top of the MembershipProvider base class and use EF code first to interact with the database.  Jon's blog post has a sort of before and after of the database, showing what the SqlMembershipProvider needed vs what the Universal Provider uses (big time clean up).  He also mentions that as of version 1.2 (version 2.0 is current as of 5/2016), these providers are "database agnostic"... but all indication are that they are only agnostic within the Microsoft Sql family... so Compact, Express, Azure, LocalDb, etc, but not other storage systems.  For that you still have to go to a custom MembershipProvider implementation it seems.

Since Universal Providers are really just a refinement of regular Membership Providers, I don't think there's much value in me dinking around with them.


Configure ASP.NET Identity


ASP.NET Identity is intended to replace the ASP.NET Membership and Simple Membership systems.  It is the default for MVC5 applications in Visual Studio 2013.  It was meant to address some of the shortcomings of the previous membership systems, such as inflexible schemas and bloated interface for MembershipProvider, the non-extensibility of SimpleMembership, and the similar limitations on Universal Providers (all briefly touched on in the Intro to ASP.NET Identity article).

That article touts many of the benefits of ASP.NET Identity.  Identity works across all of the ASP.NET frameworks and across devices, it allows for easy modification of the schema and persistance, it's more testable, and it's claims based (among other things).  However, it wasn't all hearts and roses when it was introduced.

Brock Allen wrote a pretty thorough criticism on his blog in 2013.  He praises the storage customizability, the default EF6 implementation, the use of asynchronous code, the segregation of user storage from other security code, and the external login support.  Most of the downsides seem to center on the way Identity utilizes claims (or fails to utilize them).  The treatment of passwords is also hammered, but the deal breaker is the incomplete api.  Many security features, such as email account verification, account lockout, secure password storage, two-factor auth, etc. are missing from the framework.  His final verdict is that Identity was not ready for prime time.

In March 2014, Identity 2.0 RTM was released.  A post on John Atten's blog covers the basics, and the 2.0 release seems to have addressed many of the complaints raised by Brock Allen the year before.  Two-factor auth, email confirmation, user and role management, account lock-out, and claims integration are some of the changes in the new version.

Because Identity is OWIN middleware, configuration is done through the Startup class, located in the template under the App_Start folder in the Startup.Auth.cs file.  This is a big change from the MembershipProvider based systems which were configured through the web.config file.  This is the default template configuration:

public partial class Startup
{
    // For more information on configuring authentication, please visit 
    //http://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    {
        // Configure the db context, user manager and signin manager to use a single 
        // instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(
            ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationSignInManager>(
            ApplicationSignInManager.Create);
 
        // Enable the application to use a cookie to store information for the signed in 
        // user and to use a cookie to temporarily store information about a user logging
        // in with a third party login provider
        // Configure the sign in cookie
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                // Enables the application to validate the security stamp when the user 
                // logs in.
                // This is a security feature which is used when you change a password 
                //or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator
                .OnValidateIdentity<ApplicationUserManagerApplicationUser>(
                        validateInterval: TimeSpan.FromMinutes(30),
                        regenerateIdentity: 
                            (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });            
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
 
        // Enables the application to temporarily store user information when they are 
        // verifying the second factor in the two-factor authentication process.
        app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, 
            TimeSpan.FromMinutes(5));
 
        // Enables the application to remember the second login verification factor 
        // such as phone or email.
        // Once you check this option, your second step of verification during the login 
        // process will be remembered on the device where you logged in from.
        // This is similar to the RememberMe option when you log in.
        app.UseTwoFactorRememberBrowserCookie(
            DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
 
        // Uncomment the following lines to enable logging in with third party login 
        // providers
        //app.UseMicrosoftAccountAuthentication(
        //    clientId: "",
        //    clientSecret: "");
 
        //app.UseTwitterAuthentication(
        //   consumerKey: "",
        //   consumerSecret: "");
 
        //app.UseFacebookAuthentication(
        //   appId: "",
        //   appSecret: "");
 
        //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
        //{
        //    ClientId = "",
        //    ClientSecret = ""
        //});
    }
}

While this allows for configuration as a whole, configuring the identity itself is in another file under App_Start called IdentityConfig.cs.  This file has four classes that define the EmailService, SmsService, ApplicationUserManager, and ApplicationSignInManager.  The Email and Sms service classes are empty by default.  ApplicationUserManager has one method, Create, which actually builds the manager.  It is in this class that user validation parameters, password length requirements, lockout defaults, and TwoFactor and data protection providers are configured.  The email and sms services are newed up and assigned to the manager instance.

The ApplicationSignInManager class doesn't appear to do much, just wrapping a couple calls to methods on the User and UserManager, however the generic baseclass, SignInManager, has the expected additional functionality related to signing in, including sign ins with external providers, two factor sign in, and password sign in.

One big sticking point with previous Membership frameworks was the difficulty (or impossibility) of working with non SQL Server based technologies.  Identity is structured such that the application interacts with the "Manager" classes (UserManager, RoleManager), which have no knowledge of the underlying storage implementation.  That means it's possible to implement a new storage provider without affecting the application code (sort of, there is a bit of a caveat).

That's not so say that implementing a custom storage provider is simple.  Scott Allen's blog post on the subject, as well as the article Overview of Custom Storage Providers for ASP.NET Identity cover the requirements in detail.  Using these articles and the MySql sample implementation as a guide, I created an in-memory implementation that uses static properties on the context object to "store" user data.

One thing I found interesting (if, in retrospect, unsurprising) about the exercise was that the more abstract layers, closer to the application domain, required little to no tweaking to switch from the MySql implementation to the in-memory implementation.  IdentityRole and IdentityUser are largely unchanged (just added a few read-only collections for Roles, Claims, and Logins to the IdentityUser), and the UserStore and RoleStore only required that I switch the type on the passed in database context from MySqlDatabase to InMemoryContext.  All the real work is done by the Data Access Layer classes (the Context and Table classes).

The UserStore implements a number of interfaces that define different capabilities of the provider. The overview article points out that the default project template assumes many of these interfaces are present.  I was able to user the MySql version of the UserStore almost unchanged because all the work is delegated to the DAL classes.  Here is the class diagram:



Other than the DAL and Store classes, there are a few other changes that need to be made to make the template use the custom provider.  The ReadMe for the MySql implementation was helpful in this regard:

  • Uninstall the Identity.EntityFramework NuGet package.
  • ApplicationDbContext needs to subclass InMemoryContext instead of IdentityDbContext
  • The MySql readme suggests adding an explicit cast to the dbContext passed in to the UserStore when creating the UserManager, but omitting this doesn't seem to break anything...
If you adjust the definition of ApplicationDbContext in the IdentityModels file, you shouldn't need to change anything in IdentityConfig, Startup.Auth, or the AccountController.  I think the ApplicationUser probably also should have stayed unchanged, but when I ran it the GenerateUserIdentityAsync kept throwing a NullPointerException, so I had to hack it:

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
    // Note the authenticationType must match the one defined in 
    // CookieAuthenticationOptions.AuthenticationType
 
    try
    {
        var userIdentity = await manager.CreateIdentityAsync(this, 
            DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }
    catch (Exception ex)
    {
        Trace.WriteLine(ex.StackTrace);
 
        var claims = new UserClaimsTable(new InMemoryContext()).FindByUserId(this.Id).Claims;
 
        var userIdentity = new ClaimsIdentity(claims, 
            DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.Name, null);
        userIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, this.Id));
        userIdentity.AddClaim(
        new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", 
                DefaultAuthenticationTypes.ApplicationCookie));
        userIdentity.AddClaim(new Claim(ClaimTypes.Name, this.UserName));
        
        return userIdentity;
    } 
}

Overall it was an enlightening exercise, even if the resulting code really isn't very useful.  My InMemoryIdentity project is shared on GitHub.





No comments:

Post a Comment