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