Friday, December 18, 2015

How to improve performance with ASP.NET Identity and Episerver

I have written a few blog posts now about how to implement ASP.NET Identity in Episerver. If you use the ASP.NET Identity data model out of the box, you could run into some performance issues with large quantity. With a few tricks you could optimize the data model and improve the performance a lot. I have tested this changes with a database that contains more than 400 thousand users. The performance improved with more than 10 times.

When it comes to both performance and security it’s important that the database isn’t running on the same server as the CMS.

Important note! If you already got a database model up and running and apply this changes you would need to do a migration to new database model. Here is some reference on how to fix that if you run into this problem: 


Prerequisite 

The example in this post are based on the code from here: http://sveinaandahl.blogspot.no/2015/08/how-to-integrate-aspnet-identity-with.html

Database model

It was several elements I found during the analyze that could improve the performance, but here are a few easy fixes.

  • It’s important that Datatypes in the database isn’t using  MAX, e.g. NVARCHAR<MAX>. It’s actually better to set a large number. SQL will not index datatype the contains the datatype MAX.
  • When this is fixed. You can add index on selected tables. By doing this the request dropped from 200 milliseconds to 30. 
  • Another effect by adding indexs is that the SQL server will be steady, even if the CMS frontend server gets a lot of request.

Most of this changes can be done through Entity framework. All the changes you want to apply to the data model can implemented in the OnModelCreating function.  Here you can change the datatype and add index types that will be used a lot.


Update Models/Account/ApplicationUser.cs with this code


using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Annotations;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace TestSite.Models.Account
{
    // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
    public class ApplicationUser : IdentityUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }
 
    public class MyClaims : IdentityUserClaim
    {
    }
 
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("EcfSqlConnection", throwIfV1Schema: false)
        {
            Database.SetInitializer<ApplicationDbContext>(new DropCreateDatabaseIfModelChanges<ApplicationDbContext>());
        }
 
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
 
            //Shorten length on existing datatypes => improve performance
            modelBuilder.Entity<ApplicationUser>().Property(u => u.PhoneNumber).HasMaxLength(20);
            modelBuilder.Entity<ApplicationUser>().Property(u => u.PasswordHash).HasMaxLength(1024);
            modelBuilder.Entity<ApplicationUser>().Property(u => u.SecurityStamp).HasMaxLength(1024);
            modelBuilder.Entity<MyClaims>().Property(u => u.ClaimType).HasMaxLength(512);
            modelBuilder.Entity<MyClaims>().Property(u => u.ClaimValue).HasMaxLength(512);
 
            //Indexing important datatypes. => improve performance
            modelBuilder.Entity<ApplicationUser>().Property(u => u.Email).HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()));
        }
    }
}

Happy coding!

Thursday, December 3, 2015

How to login to EPiServer with Facebook

Wouldn't it be great to be able to login with another Identity Provider, e.g. Facebook, in your Episerver solution? Which could giving you the ability to extract Facebook information from you users. In the picture below, you can see your Facebook picture and username displayed in the header on the standard Episerver Alloy demo after login.





Prerequisite 
First, you will need to create a login based on the ASP.NET Identity instead of Membership provider. You find how to do that here. This example should work with EPiServer version 8 from 14 November 2014 and up. However, I recommend updating to the latest version.

Create a connection with Facebook
You will need to setup an App on Facebook to be able to create a connection. Go to https://developers.facebook.com/apps to do that. Here is a good link to how to setup the connection step-by-step, http://www.oauthforaspnet.com/providers/facebook/

Tips! Remember that after you have create an App, you can add a platform. In this case, you would add a “website”. Since this is a test case, you can add a localhost address here. Notice the App ID and App Secret, which you will need later on.


Necessary nuget packages
You will need at least one nuget package to be able to create this connection in your project.

  • Install-Package Microsoft.Owin.Security.Facebook 

Other nuget packages that could come handy later on:

  • Install-Package Microsoft.Owin.Security.Google 
  • Install-Package Microsoft.Owin.Security.MicrosoftAccount 
  • Install-Package Microsoft.Owin.Security.Twitter 

If you want to extract more information from Facebook, you will need to install this one too.

  • Install-Package Facebook


Startup.cs
If you only want the plain login you don’t need specify anything more than the following lines in the Startup.cs.

   var facebookOptions = new FacebookAuthenticationOptions();
   facebookOptions.AppId = "xxxxxxxxxxxxxxxx";
   facebookOptions.AppSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
   app.UseFacebookAuthentication(facebookOptions);

However, if you want to extract more information through scopes and token it could look something like this:

    var facebookOptions = new FacebookAuthenticationOptions();
    facebookOptions.Scope.Add("email");
    facebookOptions.AppId = "xxxxxxxxxxxxxxxx";
    facebookOptions.AppSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    facebookOptions.Provider = new FacebookAuthenticationProvider()
    {
        OnAuthenticated = async context =>
        {
            //Get the access token from FB and store it in the database and use FacebookC# SDK to get more information about the user
            context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
        }
    };
    facebookOptions.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
    app.UseFacebookAuthentication(facebookOptions);


Extend the login screen
Since I already have styled the login screen in Episerver style, I have just extended it with a partial view to be able to login with Facebook.


Add these views to the solution.

Account/Login.cshtml
Extend login with partial view to login with facebook and other identity providers.


@using System.Web.Optimization
@model TestSite.Models.Account.LoginViewModel
 
@{
    ViewBag.Title = "Log in";
    Layout = null;
}
 
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<link type="text/css" rel="stylesheet" href="/util/styles/login.css" />
<meta name="robots" content="noindex,nofollow" />
 
<link href="../App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet" />
<link href="../App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet" />
 
 
<body class="epi-loginBody">
 
    <div class="epi-loginContainer">
        <div id="FullRegion_LoginControl">
 
            <div class="epi-loginTop">
            </div>
            <div class="epi-loginMiddle">
                <div class="epi-loginContent">
                    <div class="epi-loginLogo">@ViewBag.Title.</div>
                    <div class="epi-loginForm">
                        <h1><span style="color: Red;"></span></h1>
 
                        <div id="FullRegion_LoginControl_ValidationSummary1" style="display: none;">
 
                        </div>
                        <div class="epi-credentialsContainer">
 
 
                            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
                            {
                                @Html.AntiForgeryToken()
 
                                @Html.ValidationSummary(true)
                                <div class="epi-float-left">
                                    @Html.LabelFor(m => m.UserName, new { @class = "episize80" })
                                    <br />
                                    <div class="">
                                        @Html.TextBoxFor(m => m.UserName, new { @class = "epi-inputText" })
                                        @Html.ValidationMessageFor(m => m.UserName)
                                    </div>
                                    <span id="FullRegion_LoginControl_RequiredFieldValidator1" style="display: none;">­</span>
                                </div>
 
                                <div class="epi-float-left">
                                    @Html.LabelFor(m => m.Password, new { @class = "episize80" })
                                    <br />
                                    <div class="">
                                        @Html.PasswordFor(m => m.Password, new { @class = "epi-inputText" })
                                        @Html.ValidationMessageFor(m => m.Password)
                                    </div>
                                    <span id="FullRegion_LoginControl_RequiredFieldValidator2" style="display: none;">­</span>
                                </div>
 
                                <div class="epi-button-container epi-float-left">
                                    <span class="epi-button">
                                        <span class="epi-button-child">
                                            <input type="submit" value="Log in" class="epi-button-child-item" />
                                        </span>
                                    </span>
                                </div>
                                <div class="epi-checkbox-container">
                                    <span class="epi-checkbox">
                                        @Html.CheckBoxFor(m => m.RememberMe)
                                        @Html.LabelFor(m => m.RememberMe)
                                    </span>
                                </div>
                            }
 
                        </div>
                    </div>
                </div>
                <div class="epi-loginBottom">
                </div>
            </div>
            <div class="epi-credentialsContainer" style="margin-left: 30px; margin-bottom: 10px;">
                @Html.Partial("_ExternalLoginsListPartial", new TestSite.Models.Account.ExternalLoginViewModel() { Action = "ExternalLogin", ReturnUrl = ViewBag.ReturnUrl })
            </div>
        </div>
 
    </div>
</body>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Account/_ExternalLoginsListPartial.cshtml
List out the identity providers that has been configured.


@using Microsoft.Owin.Security
@model TestSite.Models.Account.ExternalLoginViewModel
 
<b>Log on using these Providers.</b><br />
 
@{
    var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes();
    if (loginProviders.Count() == 0)
    {
 
<p>There are no external authentication services configured. </p>
 
    }
    else
    {
        string action = Model.Action;
        string returnUrl = Model.ReturnUrl;
        using (Html.BeginForm(action, "Account", new { ReturnUrl = returnUrl }))
        {
            @Html.AntiForgeryToken()
            <div id="socialLoginList">
                <p>
                    @foreach (AuthenticationDescription p in loginProviders)
                    {
                    <button type="submit" class="btn btn-default" id="@p.AuthenticationType" name="provider" value="@p.AuthenticationType" title="Log in using your @p.Caption account">@p.AuthenticationType</button>
                    }
                </p>
            </div>
        }
    }
}

Account/ExternalLoginConfirmation.cshtml
Confirmation view after the user have logged in to facebook or other identity providers.


@using System.Web.Optimization
@model TestSite.Models.Account.ExternalLoginConfirmationViewModel
@{
    ViewBag.Title = "Register";
    Layout = null;
}
 
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<link type="text/css" rel="stylesheet" href="/util/styles/login.css" />
<meta name="robots" content="noindex,nofollow" />
 
<link href="../App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet" />
<link href="../App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet" />
 
 
<body class="epi-loginBody">
 
    <div class="epi-loginContainer">
        <div id="FullRegion_LoginControl">
 
            <div class="epi-loginTop">
            </div>
            <div class="epi-loginMiddle">
                <div class="epi-loginContent">
                    <div class="epi-loginLogo">@ViewBag.Title.</div>
                    <h3>Associate your @ViewBag.LoginProvider account.</h3>
                    <div class="epi-loginForm">
                        <h1><span style="color: Red;"></span></h1>
 
                        <div id="FullRegion_LoginControl_ValidationSummary1" style="display: none;">
 
                        </div>
                        <div class="epi-credentialsContainer">
 
                            @using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
                            {
                                @Html.AntiForgeryToken()
 
                                <h4>Association Form</h4>
                                <hr />
                                @Html.ValidationSummary(true)
                                <p class="text-info">
                                    You've successfully authenticated with <strong>@ViewBag.LoginProvider</strong>.
                                    Please enter a user name for this site below and click the Register button to finish
                                    logging in.
                                </p>
                                <br />
                                <div class="form-group">
                                    @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
                                    <div class="col-md-10">
                                        @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                                        @Html.ValidationMessageFor(m => m.UserName)
                                    </div>
                                </div>
                                <div class="form-group">
                                    <div class="col-md-offset-2 col-md-10">
                                        <input type="submit" class="btn btn-default" value="Register" />
                                    </div>
                                </div>
                            }
 
 
                        </div>
                    </div>
                </div>
                <div class="epi-loginBottom">
                </div>
            </div>
        </div>
 
    </div>
    @section Scripts {
        @Scripts.Render("~/bundles/jqueryval")
    }

Account/ExternalLoginFailure.cshtml
This view can be extended more with layout.
@{
    ViewBag.Title = "Login Failure";
    Layout = null;
}
 
<h2>@ViewBag.Title.</h2>
<h3 class="text-error">Unsuccessful login with service.</h3>

Add this line in the Header.cshtml
Add this code snippet into the header view to render out picture and name of user.

      @{Html.RenderAction("_UserPartial", "Account");}

Account/_UserPartial.cshtml
Display the user picture from facebook and name.

<div>
    @if (!string.IsNullOrEmpty(ViewBag.ProviderKey))
    {
        <img src=@Url.Content("https://graph.facebook.com/" + ViewBag.ProviderKey + "/picture?type=small") alt="@ViewBag.UserName" />
    }
</div>
<div>
    <h3>@ViewBag.UserName</h3>
</div>

Add these code lines in the Models/Account/AccountViewModels.cs
    public class ExternalLoginViewModel
    {
        public string Action { get; set; }
        public string ReturnUrl { get; set; }
    }

Add these code lines in the Controllers/AccountController.cs
You don't need to add the StoreFacebookAuthToken if you aren't going to use facebook data in your solution. It also contain code that would add the facebook user to a Episerver role.
       //
        // POST: /Account/ExternalLogin
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult ExternalLogin(string provider, string returnUrl)
        {
            // Request a redirect to the external login provider
            return new ChallengeResult(provider,
                Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
        }
 
        //
        // GET: /Account/ExternalLoginCallback
        [AllowAnonymous]
        public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
        {
            var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
            if (loginInfo == null)
            {
                return RedirectToAction("Login");
            }
 
            // Sign in the user with this external login provider if the user already has a login
            var user = await UserManager.FindAsync(loginInfo.Login);
            if (user != null)
            {
                //Save the FacebookToken in the database if not already there
                await StoreFacebookAuthToken(user);
                await SignInAsync(user, false);
                return RedirectToLocal(returnUrl);
            }
            // If the user does not have an account, then prompt the user to create an account
            ViewBag.ReturnUrl = returnUrl;
            ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
            return View("ExternalLoginConfirmation",
                new ExternalLoginConfirmationViewModel { UserName = loginInfo.DefaultUserName });
        }
 
        private async Task StoreFacebookAuthToken(ApplicationUser user)
        {
            var claimsIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
            if (claimsIdentity != null)
            {
                // Retrieve the existing claims for the user and add the FacebookAccessTokenClaim
                var currentClaims = await UserManager.GetClaimsAsync(user.Id);
                var facebookAccessToken = claimsIdentity.FindAll("FacebookAccessToken").First();
                if (!currentClaims.Any())
                {
                    await UserManager.AddClaimAsync(user.Id, facebookAccessToken);
                }
                //NB! These lines will add the user to the facebookgroup (role)
                var assingedRoles = await UserManager.GetRolesAsync(user.Id);
                if (!assingedRoles.Contains("WebAdmins") || !assingedRoles.Contains("WebEditors") || !assingedRoles.Contains("FacebookGroup"))
                {
                    await UserManager.AddToRoleAsync(user.Id, "FacebookGroup");
                }
            }
        }
 
        //
        // POST: /Account/ExternalLoginConfirmation
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model,
            string returnUrl)
        {
            if (User.Identity.IsAuthenticated)
            {
                return View(model); //ERROR?
            }
 
            if (ModelState.IsValid)
            {
                // Get the information about the user from the external login provider
                var info = await AuthenticationManager.GetExternalLoginInfoAsync();
                if (info == null)
                {
                    return View("ExternalLoginFailure");
                }
                var user = new ApplicationUser { UserName = model.UserName };
                var result = await UserManager.CreateAsync(user);
                if (result.Succeeded)
                {
                    result = await UserManager.AddLoginAsync(user.Id, info.Login);
                    if (result.Succeeded)
                    {
                        await StoreFacebookAuthToken(user);
                        await SignInAsync(user, false);
                        return RedirectToLocal(returnUrl);
                    }
                }
                AddErrors(result);
            }
 
            ViewBag.ReturnUrl = returnUrl;
            return View(model);
        }
 
        [AllowAnonymous]
        public ActionResult _UserPartial()
        {
            var userId = User.Identity.GetUserId();
            if (!string.IsNullOrEmpty(userId))
            {
                var claimsforUser = UserManager.GetClaims(userId);
                Claim firstOrDefault = claimsforUser.FirstOrDefault(x => x.Type == "FacebookAccessToken");
                if (firstOrDefault != null)
                {
                    var accessToken = firstOrDefault.Value;
                    var fb = new FacebookClient(accessToken);
                    dynamic myInfo = fb.Get("me?fields=first_name,last_name,id");
                    ViewBag.ProviderKey = myInfo.id;
                    ViewBag.UserName = myInfo.first_name + " " + myInfo.last_name;
                }
                else
                {
                    var user = await UserManager.FindByIdAsync(userId);
                    ViewBag.UserName = user.UserName;
                }
            }
            return PartialView();
        }

       // Used for XSRF protection when adding external logins
        private const string XsrfKey = "XsrfId";
 
        private class ChallengeResult : HttpUnauthorizedResult
        {
            public ChallengeResult(string provider, string redirectUri) : this(provider, redirectUri, null)
            {
            }
 
            public ChallengeResult(string provider, string redirectUri, string userId)
            {
                LoginProvider = provider;
                RedirectUri = redirectUri;
                UserId = userId;
            }
 
            public string LoginProvider { get; set; }
            public string RedirectUri { get; set; }
            public string UserId { get; set; }
 
            public override void ExecuteResult(ControllerContext context)
            {
                var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
                if (UserId != null)
                {
                    properties.Dictionary[XsrfKey] = UserId;
                }
                context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
            }
        }

        private void AddErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }

Testing the solution
So after all is implemented and compiled you will come to a facebook login screen after you hit the "facebook" button on the episerver login screen.


After you have logged in you get a form where you can register the user in the local storage, but then without the password.



After hitting the registration button you will return to the start screen where the facebook picture and name is displayed.

Database
If you look in the Identity database after you have register, you will notice the password is set to null on the facebook user.



Okey. That's it. Happy coding!



References

Thursday, November 5, 2015

How to fix the roles in EPiServer when using ASP.NET Identity



If you go into admin in EPiServer and then "Set Access Rights for", you will fail to choose your Identity 2.0 roles. Reason for this is that the Access Rights GUI in EPiServer are closely connected to the old membership provider, which you disable when you implement the ASP.NET Identity.

After a short brainstorm with my colleague Roger Nesheim, it became quite clear that the quickest way to fix this legacy problem was to use Dependency Injection and Inversion of Control. StructureMap is a DI/IOC tool exclusively for .NET programming, which we will use in this EPiServer projects. It helps in achieving looser coupling among classes and their dependencies.

Edit:
As Per suggested in the comment field, it's also possible to to implement this using built in provider instead of using StructureMap. So by add the following code in web.config and add the IdentitySecurityEntityProvider.cs to the project it will also work. Thanks :-)


  <episerver.framework>
    <securityEntity>
      <providers>
        <add name="IdentityProvider" type="TestSite.Business.AccessRights.IdentitySecurityEntityProvider, TestSite" />
      </providers>
    </securityEntity>
    <appData basePath="App_Data" />
    <scanAssembly forceBinFolderScan="true" />
    <virtualRoles addClaims="true">
      <providers>

Solution

First you need to setup the DI/IOC to override the “SecurityEntityProvider” and replace it with “IdentitySecurityEntityProvider” class. The setup below show how to do that.

ServiceLocationConfiguration.cs


using System;
using EPiServer.Security;
using StructureMap;
using TestSite.Business.AccessRights;
 
namespace TestSite.Business.ServiceLocation
{
    public static class ServiceLocationConfiguration
    {
        public static Action<ConfigurationExpression> Current
        {
            get
            {
                return configuration =>
                {
                    configuration.For<SecurityEntityProvider>().Use(() => new IdentitySecurityEntityProvider());
                };
            }
        }
    }
}

DependenciesInitializationModule.cs


using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
 
namespace TestSite.Business.ServiceLocation
{
    [InitializableModule]
    [ModuleDependency(typeof(ServiceContainerInitialization))]
    public class DependenciesInitializationModule : IConfigurableModule
    {
        public void Initialize(InitializationEngine context)
        {
        }
        public void Preload(string[] parameters) { }
        public void Uninitialize(InitializationEngine context)
        {
            //Add uninitialization logic
        }
        public void ConfigureContainer(ServiceConfigurationContext context)
        {
            context.Container.Configure(ServiceLocationConfiguration.Current);
        }
    }
}

Then you will need to override the different Search functions with new code to handle the communication with the ASP.NET Identity database. When this is in place the access rights with users and group will work as it used to.
Disclaimer: The code isn't tested on large database with a lot of users and groups.

IdentitySecurityEntityProvider.cs


using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.Security;
using TestSite.Models.Account;
 
namespace TestSite.Business.AccessRights
{
  public class IdentitySecurityEntityProvider : PagingSupportingSecurityEntityProvider
  {
    private readonly ApplicationDbContext _db = new ApplicationDbContext();
 
    public override IEnumerable<string> GetRolesForUser(string userName)
    {
        var theUser = _db.Users.FirstOrDefault(u => u.UserName.Equals(userName, StringComparison.CurrentCultureIgnoreCase));
        return theUser == null ? Enumerable.Empty<string>() : theUser.Roles.Select(role => _db.Roles.Find(role.RoleId).Name.ToString()).AsEnumerable();
    }
 
    public override IEnumerable<SecurityEntity> Search(string partOfValue, string claimType)
    {
      int totalCount;
      return this.Search(partOfValue, claimType, 0, int.MaxValue, out totalCount);
    }
 
    public override IEnumerable<SecurityEntity> Search(string partOfValue, string claimType, int startIndex, int maxRows, out int totalCount)
    {
      totalCount = 0;
      switch (claimType)
      {
        case "http://schemas.microsoft.com/ws/2008/06/identity/claims/role":
          return this.SearchRoles(partOfValue, startIndex, maxRows, out totalCount);
        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name":
        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":
          return this.SearchUsers(partOfValue, startIndex, maxRows, out totalCount);
        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":
          if (!string.IsNullOrEmpty(partOfValue))
              return this.SearchEmails(partOfValue, startIndex, maxRows, out totalCount);
          else
              return Enumerable.Empty<SecurityEntity>();
        default:
          return Enumerable.Empty<SecurityEntity>();
      }
    }
 
    private IEnumerable<SecurityEntity> SearchEmails(string partOfName, int startIndex, int maxRows, out int totalCount)
    {
        var usersList = _db.Users.ToList();
        var users = usersList.Where(a => a.Email != null && a.Email.ToLower().Contains(partOfName.ToLower())).ToList();
        var entityList = users.Select(user => new SecurityEntity(user.Email)).ToList();
        totalCount = entityList.Count;
        return entityList;
    }
 
    private IEnumerable<SecurityEntity> SearchUsers(string partOfName, int startIndex, int maxRows, out int totalCount)
    {
        totalCount = 0;
        if (_db.Users == null) return Enumerable.Empty<SecurityEntity>();
        var usersList = _db.Users.ToList();
        var entityList = !string.IsNullOrEmpty(partOfName) ? usersList.Where(a => a.UserName.ToLower().Contains(partOfName.ToLower())).ToList().Select(user => new SecurityEntity(user.UserName)).ToList() : usersList.Select(user => new SecurityEntity(user.UserName)).ToList();
        totalCount = entityList.Count;
        return entityList;
    }
 
    private IEnumerable<SecurityEntity> SearchRoles(string partOfName, int startIndex, int maxRows, out int totalCount)
    {
        totalCount = 0;
        if (_db.Roles == null) return Enumerable.Empty<SecurityEntity>();
        var rolesList = _db.Roles.ToList();
        var roles = rolesList.Where(a => a.Name.ToLower().Contains(partOfName.ToLower())).ToList();
        var entityList = roles.Select(role => new SecurityEntity(role.Name)).ToList();
        totalCount = entityList.Count;
        return entityList;
    }
  }
}

TIPS! Do not forget to add your “CustomGroups” to the correct location paths in Web.Config, e.g. the admin part. Or else the users in this group will not be able to access the edit and admin in EPiServer.


  <location path="EPiServer/CMS/admin">
    <system.web>
      <authorization>
        <allow roles="WebAdmins, Administrators, CustomGroup" />
        <deny users="*" />
      </authorization>
    </system.web>
  </location>

After you are finished with compiling, you should be able to create new custom roles in the Admin tool solution I have created earlier.



Then you can click add “Users/ group button” in the “Set Access rights” module. Choose users or group and then add it with the green arrows and click OK.


Then it will appear in the list below. Problem solved!





Happy coding!

Friday, August 14, 2015

Admin tool for ASP.NET Identity integrated with EPiServer CMS 8

Introduction

When you integrate ASP.NET Identity with EPiServer you will get a new database for the users, which will be created by the Entity Framework. First time you run the solution this database will be created and will be clean. We need an editor to be able to create and manage users and roles. Since I didn't find any editor that suited my need, I ended up building one myself. The result you can see in the picture below.


I have worked with EPiServer since early 2000's, but I can’t remember the Admin has changed much. Sure could need some love. I wish that all the links under “Access Rights” could disappear except “Set Access Rights”, when the ASP.NET membership was disabled and that you could add plugins to the “Access Rights” area. Nevertheless, for now we must add it to the “Tools” area.

NB! Before you go on, you should take a look at “How to integrate ASP.NET Identity with EPiServer 8, if you haven't read this first.

Source Code

Demo source code for this solution can be downloaded, which also includes the Admin tool:
https://github.com/saandahl/IdentityDemo

Install necessary Nuget Packages

First, we need to install some nuget packages. Open the Tools > Nuget Package Manager > Package Manager Console and run this commands:

PM> Install-Package PagedList
PM> Install-Package PagedList.Mvc
PM> Install-Package Microsoft.jQuery.Unobtrusive.Ajax

The PagedList will add a new folder “Content” and the jQuery package will add a folder called “Scripts”. All need for the Admin tool. 

Source code for the Admin Tool

To avoid getting the reward for the longest blog post ever, I stored the source code for both ASP.NET Identity integration and the Admin Tool on Github. You can find that here.

Here is an overview what files that was added or changed to make this work in the Alloy Demo:

Content > Site.css
This CSS was created to fix some missing tags, but most the CSS is reused from EPiServer.

Models > Account > AccountViewModels.cs
Includes models for both editor and login view.

Helpers > HtmlHelpers.cs
The ActivePage function was added to get the tabs correct in the editor.

Views > Identity 
This folder include all the views for the editor.

Views > Shared > Layout > _LayoutIdentity.cshtml
Master page for the editor. Some of the CSS could get wrong url if you use a different version of EPiServer. Could probably use some other CSS files. But, it works for the demo.

Web.config

NB! Do not apply this code snippet before you have created an admin user with both WebAdmins and WebEditors role.


  <location path="Identity">
    <system.web>
      <authorization>
        <allow roles="WebAdmins, Administrators" />
        <deny users="*" />
      </authorization>
    </system.web>
  </location>

How to create an user?

So when you can got all the files and code in place and build is succesfull. Open the startpage and then add "/Identity/Index" in the URL. If you got all the code correct you should be able to get something like this up in you browser:


Then click on the Create User tab and create a new account.


Then go to the Manage Roles tab. Add both “WebAdmins” and “WebEditors”.


Then Click on the Account overview tab and hit “Roles for user” button. Here you can add the roles you just added.


Now you should be able to login at this URL /Account/Login.

NB! Don’t forget to add the web.config snippet after you have verify that your user have managed to login or else everyone can do the same as you just did.




How to integrate ASP.NET Identity with EPiServer 8

Introduction

The standard way of authenticate in EPiServer with the membership provider version has been used for more than a decade and has become very limited to how modern authenticate and authorization is done.

I believe that several of us has customers that request new functionality that we cannot provide with the standard EPiServer membership provider. I often see and hear that they want to be able to login with Facebook or other Identity providers. They want to be able to login with cusomter number, phone number og email as the user name.  Could we customize the identity data model with more fields? Could we have a two-factor login process, including sms?

And more..

By using ASP.NET Identity, you will be able to solve these requests. To avoid getting a very large blog post I have divide it into several.

  • First will be how you integrate the ASP.NET Identity in a standard EPiServer Alloy Demo with a login screen.
  • Second, will contain how to create an Admin tool to create and manage users and roles.
  • Third, will be about how you optimize and migrate data from the membership to the Identity model. Yes, by optimize the data model you could improve the performance a lot. I'll also show how you can keep the old passwords after migration.

For those who is not familiar with ASP.NET Identity this could be a good place to start here

In addition, ASP.NET Identity has its fall pits, which Brock has summery up here.

First, we need a demo project. For those of you are not familiar with how to setup an EPiServer CMS Alloy demo with MVC, here is a quick introduction.

Source Code

Demo source code for this solution can be downloaded, which also includes the Admin tool:
https://github.com/saandahl/IdentityDemo


Create an EPiServer site

First, you will need to install the EPiServer extensions in Visual Studio. Which you can find here.

This will give you the ability to create new EPiServer sites. Just open Visual Studio, go to File and select New and Project. Then select Templates, Visual C#, EPiServer, and EPiServer Web Site, and provide a name.


Then select Alloy (MVC) for a site with sample content.


Hit “OK” to finalize the creation of the project. Then press F5 to build and open the website in a browser. Then the site should come up and look like this:


NB! To avoid any pitfalls later on I suggest that you restore the database (.mdf files) from the App_Data folder to an SQL server. ASP.NET Identity will also need its own database, but this will be generated from Entity Framework when you start it up the first time, if you got the correct access rights.

I also recommend setting up a site in IIS and add Network Service account to both SQL and IIS application pool identity.

The only thing you would need to change is the connectionString in the web.config. You will find example on how it could look like further down.

Install necessary Nuget Packages

So far so good. Now we need to install some nuget packages. Open the Tools > Nuget Package Manager > Package Manager Console and run this commands:

PM> Install-Package Microsoft.Owin.Security.Cookies
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.AspNet.Identity.EntityFramework
PM> Install-Package Microsoft.AspNet.Identity.Owin
PM> Install-Package Microsoft.AspNet.WebApi


This will download the packages and add the necessary references to the project and all the packages references will be added to the packages.config.

The marked files/folders are the only places you will need to add or modify the code to get this up and running.


App_Start > IdentityConfig.cs

This is where you can tighten or loosen up the password strengths. To be able to use small test password I have set it very low.


using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using TestSite.Models.Account;
 
namespace TestSite
{
    // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application.
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(ApplicationUserStore store)
            : base(store)
        {
        }
 
        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
        {
            var manager = new ApplicationUserManager(new ApplicationUserStore(context.Get<ApplicationDbContext>()));
            // Configure validation logic for usernames
            manager.UserValidator = new UserValidator<ApplicationUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                //RequireUniqueEmail = true
            };
 
            // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = false,
                RequireUppercase = false,
            };
 
            // Configure user lockout defaults
            manager.UserLockoutEnabledByDefault = true;
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
            manager.MaxFailedAccessAttemptsBeforeLockout = 5;
 
            // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
            // You can write your own provider and plug it in here.
            manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
            {
                MessageFormat = "Your security code is {0}"
            });
            manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
            {
                Subject = "Security Code",
                BodyFormat = "Your security code is {0}"
            });
 
            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider =
                        new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
            }
            return manager;
        }
    }
 
    // Configure the application sign-in manager which is used in this application.
    public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
    {
        public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
            : base(userManager, authenticationManager)
        {
        }
 
        public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
        {
            return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
        }
 
        public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options,
            IOwinContext context)
        {
            return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
        }
    }
 
    public class ApplicationUserStore : UserStore<ApplicationUser>
    {
        public ApplicationUserStore(ApplicationDbContext ctx)
            : base(ctx)
        {
        }
    }
 
    public class ApplicationRoleStore : RoleStore<IdentityRole>
    {
        public ApplicationRoleStore(ApplicationDbContext ctx)
            : base(ctx)
        {
        }
    }
 
    public class ApplicationRoleManager : RoleManager<IdentityRole>
    {
        public ApplicationRoleManager(ApplicationRoleStore roleStore)
            : base(roleStore)
        {
        }
    }
}

Controllers > AccountController.cs


using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using TestSite.Models.Account;
 
namespace TestSite.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        private ApplicationSignInManager _signInManager;
        private ApplicationUserManager _userManager;
 
        public AccountController()
        {
        }
 
        public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
        {
            UserManager = userManager;
            SignInManager = signInManager;
        }
 
        public ApplicationSignInManager SignInManager
        {
            get
            {
                return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
            }
            private set
            {
                _signInManager = value;
            }
        }
 
        public ApplicationUserManager UserManager
        {
            get
            {
                return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
            }
            private set
            {
                _userManager = value;
            }
        }
 
        //
        // GET: /Account/Login
        [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }
 
        //
        // POST: /Account/Login
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                var user = await UserManager.FindAsync(model.UserName, model.Password);
                if (user != null)
                {
                    await SignInAsync(user, model.RememberMe);
                    return RedirectToLocal(returnUrl);
                }
                ModelState.AddModelError("", "Invalid username or password.");
            }
 
            // If we got this far, something failed, redisplay form
            return View(model);
        }
 
        //
        // POST: /Account/LogOff
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            Request.GetOwinContext().Authentication.SignOut();
            return Redirect("/");
        }
   
        protected override void Dispose(bool disposing)
        {
            if (disposing && UserManager != null)
            {
                UserManager.Dispose();
                UserManager = null;
            }
            base.Dispose(disposing);
        }
 
        #region Helpers
 
        private IAuthenticationManager AuthenticationManager
        {
            get { return HttpContext.GetOwinContext().Authentication; }
        }
 
        private async Task SignInAsync(ApplicationUser user, bool isPersistent)
        {
            AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
            var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
            AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, identity);
        }
 
   
        private ActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            return Redirect("/");
        }
 
        #endregion
 
    }
}

Models > Account > AccountViewModels.cs


using System.ComponentModel.DataAnnotations;
 
namespace TestSite.Models.Account
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
 
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
 
        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
}

Models > Account > ApplicationUser.cs

using System.Data.Entity;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace TestSite.Models.Account
{
    // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
    public class ApplicationUser : IdentityUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }
 
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("EcfSqlConnection", throwIfV1Schema: false)
        {
            Database.SetInitializer<ApplicationDbContext>(new DropCreateDatabaseIfModelChanges<ApplicationDbContext>());
        }
 
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }
}

Views > Account > Login.cshtml

@using System.Web.Optimization
@model TestSite.Models.Account.LoginViewModel
@{
    ViewBag.Title = "Log in";
    Layout = null;
}
 
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<link type="text/css" rel="stylesheet" href="/util/styles/login.css" />
<meta name="robots" content="noindex,nofollow" />
 
<link href="../App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet" />
<link href="../App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet" />
 
 
<body class="epi-loginBody">
 
    <div class="epi-loginContainer">
        <div id="FullRegion_LoginControl">
 
            <div class="epi-loginTop">
            </div>
            <div class="epi-loginMiddle">
                <div class="epi-loginContent">
                    <div class="epi-loginLogo">@ViewBag.Title.</div>
                    <div class="epi-loginForm">
                        <h1><span style="color: Red;"></span></h1>
 
                        <div id="FullRegion_LoginControl_ValidationSummary1" style="display: none;">
 
                        </div>
                        <div class="epi-credentialsContainer">
 
 
                            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
                            {
                                @Html.AntiForgeryToken()
 
                                @Html.ValidationSummary(true)
                                <div class="epi-float-left">
                                    @Html.LabelFor(m => m.UserName, new { @class = "episize80" })
                                    <br />
                                    <div class="">
                                        @Html.TextBoxFor(m => m.UserName, new { @class = "epi-inputText" })
                                        @Html.ValidationMessageFor(m => m.UserName)
                                    </div>
                                    <span id="FullRegion_LoginControl_RequiredFieldValidator1" style="display: none;">­</span>
                                </div>
 
                                <div class="epi-float-left">
                                    @Html.LabelFor(m => m.Password, new { @class = "episize80" })
                                    <br />
                                    <div class="">
                                        @Html.PasswordFor(m => m.Password, new { @class = "epi-inputText" })
                                        @Html.ValidationMessageFor(m => m.Password)
                                    </div>
                                    <span id="FullRegion_LoginControl_RequiredFieldValidator2" style="display: none;">­</span>
                                </div>
 
                                <div class="epi-button-container epi-float-left">
                                    <span class="epi-button">
                                        <span class="epi-button-child">
                                            <input type="submit" value="Log in" class="epi-button-child-item" />
                                        </span>
                                    </span>
                                </div>
                                <div class="epi-checkbox-container">
                                    <span class="epi-checkbox">
                                        @Html.CheckBoxFor(m => m.RememberMe)
                                        @Html.LabelFor(m => m.RememberMe)
                                    </span>
                                </div>
                            }
 
                        </div>
                    </div>
                </div>
                <div class="epi-loginBottom">
                </div>
            </div>
        </div>
    </div>
</body>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Startup.cs

using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using System;
using System.Web.Helpers;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System.Security.Claims;
using System.Threading.Tasks;
using TestSite.App_Start;
using TestSite.Models.Account;
 
 
[assembly: OwinStartup(typeof(TestSite.Startup))]
 
namespace TestSite
{
    public class Startup
    {
        private const string LogoutUrl = "/util/logout.aspx";
 
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
 
        // 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<ApplicationUserManager, ApplicationUser>(
                            validateInterval: TimeSpan.FromMinutes(30),
                            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
                },
                LogoutPath = new PathString("/Account/LogOff")
            });
 
            app.Map(LogoutUrl, map =>
            {
                map.Run(ctx =>
                {
                    ctx.Authentication.SignOut();
                    ctx.Response.Redirect("/");
                    return Task.FromResult(0);
                });
            });
 
            //Tell antiforgery to use the name claim
            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
 
            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();
        }
    }
}

InitializationRouting.cs

Instead of handle the routing in the Global.asax you can add this as a InitialeizableModule. Have added the routing for the admin tool in this part too.


using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
 
namespace TestSite
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class InitializationRouting : IInitializableModule
    {
        public static void Register(HttpConfiguration config)
        {
            // Attribute routing.
            config.MapHttpAttributeRoutes();
 
            RouteTable.Routes.MapRoute(
                name: "IdentityEditor",
                url: "Identity/{action}/{id}",
                defaults: new { controller = "Identity", action = "index", id = UrlParameter.Optional }
            );
 
            RouteTable.Routes.MapRoute(
                name: "Account",
                url: "Account/{action}/{id}",
                defaults: new { controller = "Account", action = "login", id = UrlParameter.Optional }
            );
 
        }
 
        public void Initialize(InitializationEngine context)
        {
            GlobalConfiguration.Configure(Register);
        }
        public void Preload(string[] parameters) { }
        public void Uninitialize(InitializationEngine context) { }
    }
}

Web.config



    <!--Change : You don't have to change the login code in the Alloy demo as long as you add the new loginurl.-->
    <authentication mode="None">
      <forms loginUrl="~/Account/Login" timeout="2880" />
    </authentication>
 
    <!--Change-->
    <membership enabled="false">
      <providers>
        <clear />
      </providers>
    </membership>
    <roleManager enabled="false">
      <providers>
        <clear />
      </providers>
    </roleManager>
  </system.web>
  <system.webServer>
 
    <!--Change-->
  <connectionStrings>
    <add name="EPiServerDB" connectionString="Data Source=<host>;Initial Catalog=EPiServerDB_4380c2ee;Integrated Security=False;User ID=dbUser;Password=*****;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
    <add name="EcfSqlConnection" connectionString="Data Source=<host>;Initial Catalog=TestSiteIdentityDB;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>


Now you could either add ~/Account/Login in the url or click on the login button in the Alloy Demo. Then something like this should pop up.


This solution could now easily be extended with "my page" functionality where the user could manage his own information. I haven't added this to keep the example as small as possible.

How to login?

The major problem now is that you don’t have any account to login with. Check out the Admin tool i have built to create and maintain users and roles for ASP.NET Identity. Read more about that here.