Tutorial: Mvc application using Azure acs and forms authentication Part 3


This is the final post in a series of posts on converting an asp.net mvc project to use both forms authentication and Azure appFabric Access Control Service (ACS) authentication. The first post focused on creating the project, configuring the site for acs and forms authentication and setting up the database. This second poste focused on the association process and the hybrid form where users can choose between forms authentication and ACS authentication. This final post will add some more steps to the original workflow which will cover user signup.

This simplifies the process for non-members who would otherwise have to signup then logout to begin the association process.

Original workflow:

New Workflow:

Open the MVC3MixedAuthenticationSample from the two previous posts and make the following changes:

Open Controllers\AccountController.cs and replace the Register(RegisterModel model) action with the following:

        [HttpPost]
        public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                // Attempt to register the user
                MembershipCreateStatus createStatus;
                Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);

                if (createStatus == MembershipCreateStatus.Success)
                {
                    var claim = new IdentityClaim();
                    if (claim.HasIdentity)
                    {
                        var db = new IdentityRepository();
                        var user = Membership.GetUser(model.UserName);
                        db.MapIdentity(user.ProviderUserKey.ToString(), claim.IdentityProvider, claim.IdentityValue);
                        IdentityClaim.ClearSession();
                    }

                    FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
                    return RedirectToAction("Index", "Home");
                }
                else
                {
                    ModelState.AddModelError("", ErrorCodeToString(createStatus));
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

Open Views\Account\LogOn.cshtml and find the following:

 $('#AssociateCancel').click(CancelAssociation);

Immediately below insert the following code:

         $('#AssociateRegister').click(function () {
            window.location = '@Url.Action("Register")';
        });

Next Find the following code:

<div id="AssociationMessage">
    <div id="AssociateMessageText">Please login to associate your account</div>
    <div id="AssociateMessageActions"><button id="AssociateCancel">Cancel</button></div> 
    <div class="clear"></div>
</div>

Immediately below insert the following code:

 <div id="AssociationMessageRegister">
    <div id="AssociateMessageRegisterText">Click Register if you don't already have an account</div>
    <div id="AssociateMessageRegisterActions"><button id="AssociateRegister">Register</button></div> 
    <div class="clear"></div>
</div>

Open Content\HrdPage.css and insert the following css:

#AssociateMessageRegisterActions
{
    float:right;
}

#AssociateMessageRegisterText
{
    float:left;
}

#AssociationMessageRegister
{
    border: 1px solid #9F6000;
    background-color: #FEEFB3;
    color:#9F6000;
    padding:5px;
    width:400px;
}

Next run the site and click one of the provider buttons and login with a provider account that is not already associated. You can also go to Account/Identites and remove any existing associations if you don’t have another one to spare.

When you return to the site your login page should look like this:

Register as normal:

Once the user completes registration they will be able to use their identity provider to access the site.

Tutorial: Mvc application using Azure acs and forms authentication Part 2


This is the second post in a series of posts on converting an asp.net mvc project to use both forms authentication and Azure appFabric Access Control Service (ACS) authentication. The first post focused on creating the project, configuring the site for acs and forms authentication and setting up the database. This post will focus on the association process and the hybrid form where users can choose between forms authentication and ACS authentication.

Important code from ACS Mvc3 sample

Next you will need some code from the MVC3CustomSignInPage sample. Download the source from acs.codeplex.com (please note that these samples are under the Apache 2 license). We will be making changes to some of these files but I thought it would be good for people to know their origin. Extract the package and browse to the Websites\MVC3CustomSignInPage\MVC3CustomSignInPage directory and do the following.

  • Copy the Hrd folder to your project
  • Copy the Util folder to your project
  • Copy Models\HrdIdentityProvider.cs to the Models folder in your project
  • Open Controllers\AccountController.cs and copy the GetUrlFromContext method to your AccountController.cs
  • Copy Views\Account\_IdentityProvidersWithServerSideCode.cshtml to Views\Account in you project.
  • Copy Content\HrdPage.css to Content in your project

Now we should have all the code we need to complete the project.

Hybrid logon page

You must set the request validation mode in the web.config else you will get an error upon returning from ACS. Open web.config and find the following:

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

Below insert the following

<httpRuntime requestValidationMode="2.0" />

Add a section to the layout for any custom styles we will render in our views. And modify our menu to include a link to a list of associated identities. Open Views\Shared\_Layout.cshtml add replace the code with the following:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    @RenderSection("Styles",false)
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <div id="header">
            <div id="title">
                <h1>My MVC Application</h1>
            </div>
            <div id="logindisplay">
                @Html.Partial("_LogOnPartial")
            </div>
            <div id="menucontainer">
                <ul id="menu">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("Identities", "Identities", "Account")</li>
                </ul>
            </div>
        </div>
        <div id="main">
            @RenderBody()
        </div>
        <div id="footer">
        </div>
    </div>
</body>
</html>

Open Views\Account\LogOn.cshtml and replace the code with the following:

@using ASPNETSimpleMVC3.Models;
@model MVC3MixedAuthenticationSample.Models.LogOnModel

@{
    ViewBag.Title = "Log On";
}

@section Styles {
    <link href="@Url.Content("~/Content/HrdPage.css")" rel="stylesheet" type="text/css" />
    <style type="text/css">
        #FormsAuth 
        {
            width:400px;
            margin:0;
            padding:0;
            float:left;
        }
    </style>
}



<h2>Log On</h2>
<p>
    Please enter your user name and password. @Html.ActionLink("Register", "Register") if you don't have an account.
</p>

@*Needed for Visual Studio Intellisense*@
@if (false)
{
    <script src="/Scripts/jquery-1.5.1-vsdoc.js" type="text/javascript"></script>
}

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script type="text/javascript">
    $(function () {
        $('#AssociateCancel').click(CancelAssociation);
    });

    function CancelAssociation() {
        $.post('@Url.Action("CancelAssociation")',function(data){
           if(data) {
          
            $('#identity-provider-content').show();
            $('#AssociationMessage').hide();
           } 
           else{
            alert("Unexpected error. Try again later");
           }
        });
        
    }
    
</script>


@Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.")
@if (ViewData["HasClaim"]!=null)
{
<div id="AssociationMessage">
    <div id="AssociateMessageText">Please login to associate your account</div>
    <div id="AssociateMessageActions"><button id="AssociateCancel">Cancel</button></div> 
    <div class="clear"></div>
</div>
}
@using (Html.BeginForm()) {
    <div id="FormsAuth">
        <fieldset>
            <legend>Account Information</legend>

            <div class="editor-label">
                @Html.LabelFor(m => m.UserName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.UserName)
                @Html.ValidationMessageFor(m => m.UserName)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Password)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.Password)
                @Html.ValidationMessageFor(m => m.Password)
            </div>

            <div class="editor-label">
                @Html.CheckBoxFor(m => m.RememberMe)
                @Html.LabelFor(m => m.RememberMe)
            </div>

            <p>
                <input type="submit" value="Log On" />
            </p>
        </fieldset>

    </div>
    
}
@Html.Partial("_IdentityProvidersWithServerSideCode")

Open Views\Account\_IdentityProvidersWithServerSideCode.cshtml and replace the code with the following:

@using ASPNETSimpleMVC3.Models;
@{IEnumerable<HrdIdentityProvider> Providers = null;}
@{
    if (!string.IsNullOrEmpty(ViewData["Providers"].ToString()) && (ViewData["Providers"] as IEnumerable<HrdIdentityProvider>).Count() > 0)
    { 
        Providers = ViewData["Providers"] as IEnumerable<HrdIdentityProvider>;
    }
}
  

@*Needed for Visual Studio Intellisense*@
@if (false)
{
    <script src="/Scripts/jquery-1.5.1-vsdoc.js" type="text/javascript"></script>
}

@if (Providers != null)
{
<div id="identity-provider-content"  class="@(ViewData["HasClaim"] != null ? "provider-content-hidden" : "")">
    
    
    <h2>
        Or sign in using
    </h2>
    <div id="identity-providers">
        @foreach (HrdIdentityProvider identityProvider in Providers)
        {
            <div  class="identity-provider"><button onclick="window.location='@Html.Raw(identityProvider.LoginUrl)'">@identityProvider.Name</button></div>
        }
    </div>
</div>

<div class="clear"></div>
}

Open Content/HrdPage.css and change the code to the following:


#identity-provider-content
{
    width:250px;
    text-align:center;   
    float:left;
   
}

#identity-providers
{
    text-align:center;
    width:100%;
}

div.identity-provider
{

    text-align:center;
    margin: 10px 0px 10px 0px;
}

div.identity-provider button
{
    padding:10px;
    width:150px;
    vertical-align:middle;
}

#AssociationMessage
{
    border: 1px solid #4F8A10;
    background-color: #DFF2BF;
    color:#4F8A10;
    padding:5px;
    width:400px;
}

.provider-content-hidden
{
    display:none;
}

#AssociateMessageActions
{
    float:right;
}
#AssociateMessageText
{
    float:left;
}

Open Controllers\AccountController.cs and make the following changes:

Add an action called SignIn

        /// <summary>
        /// Action used to sign user in from ACS
        /// </summary>
        /// <param name="forms"></param>
        /// <returns></returns>
        [HttpPost]
        [ValidateInput(false)]
        public ActionResult SignIn(FormCollection forms)
        {
            //Extract claims
            var principal = HttpContext.User;
            var claim = new IdentityClaim(principal.Identity as IClaimsIdentity);

            //Delete session cookie so the module cannot reset principal
            SessionAuthenticationModule sam = FederatedAuthentication.SessionAuthenticationModule;
            sam.DeleteSessionTokenCookie();

            if (claim.HasIdentity)
            {
                var db = new IdentityRepository();

                var identity = db.FindByProvderAndValue(claim.IdentityProvider, claim.IdentityValue);
                string returnUrl = GetUrlFromContext(forms) ?? Url.Action("Index", "Home", null);
                if (identity != null)
                {
                    var user = Membership.GetUser(identity.UserId);

                    if (user != null)
                    {
                        FormsAuthentication.SetAuthCookie(user.UserName, false);
                        return Redirect(returnUrl);
                    }

                }
                else
                {
                    //Save identity values for processing in the association page
                    claim.SaveToSession();

                    return RedirectToAction("LogOn", new { ReturnUrl = returnUrl });

                }

            }

            return RedirectToAction("LogOn", "Account");
        }

Still in Controllers\AccountController.cs add the following actions which will facilitate the association process.

         /// <summary>
        /// Remove pending association from session
        /// </summary>
        /// <returns></returns>
        [Authorize]
        public JsonResult CancelAssociation()
        {
            IdentityClaim.ClearSession();

            return Json(true);
        }

        /// <summary>
        /// Allows a user to remove an identity association
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [Authorize]
        public JsonResult RemoveAssociation(int id)
        {
            var db = new IdentityRepository();
            var user = Membership.GetUser();
            db.DeleteById(id,user.ProviderUserKey.ToString());

            return Json(true);
        }

        /// <summary>
        /// Allows users to manage their associated identites
        /// </summary>
        /// <returns></returns>
        [Authorize]
        public ActionResult Identities()
        {
            var db = new IdentityRepository();
            var user = Membership.GetUser();

            var identities = db.FindByUserId(user.ProviderUserKey.ToString());

            return View(identities);
        }

Add a new view to Views\Account called Identities.cshtml and insert the following code:

@model IEnumerable<MVC3MixedAuthenticationSample.UserIdentity>
@using MVC3MixedAuthenticationSample.Models

@{
    ViewBag.Title = "Identities";
}

@*Needed for Visual Studio Intellisense*@
@if (false)
{
    <script src="/Scripts/jquery-1.5.1-vsdoc.js" type="text/javascript"></script>
}

<script type="text/javascript">
    function RemoveAssociation(element, id) {
        $.post('@Url.Action("RemoveAssociation")', { id: id }, function (data) {
            if (data) {
                $(element).parent().slideUp().Remove();
            }
            else {
                alert("There was an unexpected error. Try again later");
            }
        });
    }
</script>
<h2>Identities</h2>

<div id="ProviderList">
@foreach (var item in Model) {
  <div>
    @IdentityClaim.ProviderNiceName( item.IdentityProvider )  <button onclick="RemoveAssociation(this,@item.IdentityID)">Remove</button>
  </div>
}
</div>



Replace the parameter-less LogOn() action with the following code:

        public ActionResult LogOn()
        {
            HrdClient hrdClient = new HrdClient();

            WSFederationAuthenticationModule fam = FederatedAuthentication.WSFederationAuthenticationModule;
            HrdRequest request = new HrdRequest(fam.Issuer, fam.Realm, context: Request.QueryString["ReturnUrl"]);

            IEnumerable<HrdIdentityProvider> hrdIdentityProviders = hrdClient.GetHrdResponse(request);

            var claim = new IdentityClaim();
            if (claim.HasIdentity)
            {
                ViewData["HasClaim"] = true;
            }

            ViewData["Providers"] = hrdIdentityProviders;
            return View();
        }

Replace the LogOn(LogOnModel model, string returnUrl) action with the following code:

        [HttpPost]
        public ActionResult LogOn(LogOnModel model, string returnUrl)
        {
            var claim = new IdentityClaim();

            if (ModelState.IsValid)
            {
                if (Membership.ValidateUser(model.UserName, model.Password))
                {
                   

                    if (claim.HasIdentity)
                    {
                        var db = new IdentityRepository();
                        var user = Membership.GetUser(model.UserName);
                        db.MapIdentity(user.ProviderUserKey.ToString(), claim.IdentityProvider, claim.IdentityValue);
                        IdentityClaim.ClearSession();
                    }

                    FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                    if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    ModelState.AddModelError("", "The user name or password provided is incorrect.");
                }
            }

            HrdClient hrdClient = new HrdClient();

            WSFederationAuthenticationModule fam = FederatedAuthentication.WSFederationAuthenticationModule;
            HrdRequest request = new HrdRequest(fam.Issuer, fam.Realm, context: Request.QueryString["ReturnUrl"]);

            IEnumerable<HrdIdentityProvider> hrdIdentityProviders = hrdClient.GetHrdResponse(request);

            if (claim.HasIdentity)
            {
                ViewData["HasClaim"] = true;
            }

            ViewData["Providers"] = hrdIdentityProviders;
            // If we got this far, something failed, redisplay form
            return View(model);
        }

Next open controllers\HomeController.cs and add the authorize attribute so we can test our new feature.

    [Authorize]
    public class HomeController : Controller
    {
      ...
    }

Add a new class called IdentityRepository.cs to the models folder and insert the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MVC3MixedAuthenticationSample.Models
{
     
    public class IdentityRepository 
    {
        private ASPNETDBEntities _context;

        public IdentityRepository()
        {
            _context = new ASPNETDBEntities();
        }

        public int MapIdentity(string userid, string identytProvider, string identityValue)
        {
            var mapping = new UserIdentity();
            mapping.UserId = new Guid(userid);
            mapping.IdentityProvider = identytProvider;
            mapping.IdentityValue = identityValue;
            _context.AddToUserIdentities(mapping);
            _context.SaveChanges();

            return mapping.IdentityID;
        }

        public IQueryable<UserIdentity> FindByUserId(string userId)
        {
           return _context.UserIdentities.Where(i => i.UserId == new Guid(userId));
        }

        public UserIdentity FindByProvderAndValue(string IdentityProvider, string identityValue)
        {
            return _context.UserIdentities.FirstOrDefault(i => i.IdentityProvider == IdentityProvider && i.IdentityValue == identityValue);
        }

        public void DeleteById(int id, string userId)
        {
            var identity = _context.UserIdentities.FirstOrDefault(i => i.IdentityID == id && i.UserId == new Guid(userId));
            if (identity != null)
            {
                _context.DeleteObject(identity);
            }
            _context.SaveChanges();
        }

       
    }
}

At this point the solution should look like this:

Next run the project and verify that the LogOn page looks like the following:

At this point you must verify that ACS is setup properly with the project url as a valid relying party (Please see the original ACS MVC3 sample tutorial if you have not been able to set this up) and has http://%5Bprojecturl%5D/Account/SignIn as the return url. If this is not configured properly you will get a bad request exception when attempting to load the Login page.

If you have not already selected the providers you want users to authenticate against you should do so at this point. Again, there are many tutorials out there which cover this process. Facebook is the only provider which requires extra steps to be added as a provider. You can read about it here.

You should now be able to click on one of the provider buttons on the LogOn page where you will sign in using any of the providers setup in your ACS account. After signing in you will return to the following screen on your site which prompts you to login to complete the association process:

After you login you should see your new identity listed on the Identites page.

Finally, you should be able to logout then click the same provider button you clicked before to get logged in without having to enter your username and password for the site.

The sample project used in this tutorial is available on codeplex for anyone who wants it. Have fun with ACS and Asp.net memberships.

To use the sample project you will need to configure the Windows Identity framework module for your own ACS namespace.

Azure Acs plus asp.net MVC memberships


Over the past week I have been trying to get up to speed with the concept of claims aware applications, the windows identity foundation framework and the Azure AppFabric access control service. I was asked to look into this as an option for allowing customers to login to our website with their existing windows live, Google, Yahoo and Facebook logins. I had the following requirements:

  • Minimal effort in terms of new code,classes or complexity added.
  • Minimal changes to existing login/authentication workflow (our site uses a custom asp.net membership provider but still uses iPrincipal).
  • The login page should have the login/password control and the links/buttons to login using Google/Yahoo/Windows Live/Facebook.
  • This should work on any of our website’s wildcard subdomains.

After Googling, reading MSDN and doing a couple walkthroughs I finally figured out a way to achieve most of my requirements. During my search I found quite a few people trying to find a way to add single signon services to their asp.net websites using ACS (Azure App Fabric Access Control Service) and other Secutirty Token Services (STS). Although it doesn’t fulfill all of my requirements, I figured someone might find what I came up with useful. I have not figured out how to fulfill the final requirement because ACS requires the replying party information before hand. There is no room for wildcard urls and therefore I have no idea how I  could allow a single site such as a dotnetnuke site or a website with wildcard subdomains to act as a relying party for all child sites. If anyone has any ideas let me know. I am thinking ACS OAuth2 but I have not looked into it yet.

If you have an existing website and simply want to add a “login with facebook” button on your login page, I doubt you are looking to  delegate your entire authentication and authorization system to ACS or any other external STS.  Unfortunately, the samples and documentation I came across (Very good BTW) cover the broader concept of federation and claims both as an option for removing authorization and authentication logic from your application and streamlining trust relationships between internal an external parties. The parties in the examples usually have most if not all the claims required for a user to interact with a system. Unfortunately, The default identity providers available in the ACS ( Facebook, Yahoo, Google, Windows Live) only offer enough information to identify a user by name, email or a unique id. Windows live only offers the latter. This means that you can’t use their limited claimset for anything more than an identifier (this is really all we need). My goal here is to allow users to associate their identities to their existing asp.net membership accounts by adding a few steps to the existing authentication workflow.

The following graphic is a simplified representation of the workflow.

In the workflow, the “website ACS signin page” is the url you tell ACS to send the user to after they have been successfully authenticated. This is the place where I check the identity claim against the database and sign in the appropriate user. By the time execution gets to the “website ACS signin page” the ClaimsPrincipal is set and the authentication module cookie is set so after I get the identity claims from the ClaimsPrincipal I delete the authentication module cookie and reset the principal to the associated user. If the user is not already associated, they are taken through the association process. It is also possible to add a new user signup to the association process.  I decided to store the association information in a table called UserIdentity which I  access with Entity framework using linq. This table has a row for each of the user’s identities (1 for google, 1 for yahoo, 1 for windows live etc).

Table Structure

Sample Data

Corresponding linq query on “website ACS signin page”.

//Extract claims
var identityClaim = new MyIdentityClaim(HttpContext.user as ClaimsPrincipal);

//Delete session cookie so the module cannot reset principal
SessionAuthenticationModule sam = FederatedAuthentication.SessionAuthenticationModule;
sam.DeleteSessionTokenCookie();

if(identityClaim.HasIdentity)
{
var db = new identityEntities();
var userid = (from ui in db.UserIdentity
               where ui.IdentityValue == identityCliam.IdentityValue && ui.IdentityProvider == identityClaim.IdentityProvider
               select ui.UserId
             ).FirstOrDefault();

   var user = Membership.GetUser(userid);
   if(user!=null)
   {
         FormsAuthentication.SetAuthCookie(user.UserName, false);
   }
   else
   {
       //Save identity values for processing in the association page
       identityClaim.SaveToSession();

      //Send user into the association process
      return RedirectToAction("Associate", "Account");
   }

}

EDIT:
Here is the code for MyIdentityClaim


    public class MyIdentityClaim
    {
        private string _identityProvider;
        private string _identityValue ;
        public const string ACSProviderClaim = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";

        public MyIdentityClaim(IClaimsIdentity identity)
        {
           
            if (identity != null)
            {
                foreach (var claim in identity.Claims)
                {
                    if (claim.ClaimType == ClaimTypes.NameIdentifier)
                    {
                        _identityValue = claim.Value;
                    }
                    if (claim.ClaimType == ACSProviderClaim)
                    {
                        _identityProvider = claim.Value;
                    }

                }
            }


        }

        public bool HasIdentity
        {
            get
            {
                return (!string.IsNullOrEmpty(_identityProvider) && (!string.IsNullOrEmpty(_identityValue)));
            }
        }

        public string IdentityProvider
        {
            get
            {
                return _identityProvider;
            }
        }

        public string IdentityValue
        {
            get
            {
                return _identityValue;
            }
        }


    }

Now you can  add your “login with facebook” button on your Aspnet member ship website with minimal changes to your existing code. Here is my modified signin page.

I used the MVC3CutomSigninPage sample found here as a starting point for my proof of concept. This sample also shows you how to get the list of available providers from ACS to create the blue buttons seen above. I don’t have time to create a full walkthrough but I believe the information I provided should help people get over that hump. I will be happy to answer questions.

EDIT:
Download the acs samples here. The MVC3CutomSigninPage sample can be found in the ‘Websites’ folder. The online readme file for this sample can be found here. You can add the code I provided after getting the sample to work.

After you have confirmed acs is working you can change the web.config back to forms authentication:

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

Also make sure passive redirect is disabled:

<wsFederation passiveRedirectEnabled="false" issuer="https://[namespace].accesscontrol.windows.net/v2/wsfederation" realm="https://yoursite.com/" requireHttps="true" />