End-to-end authentication and authorisation scenario for MVC+ACS

Background

Windows Azure’s Access Control Service (ACS) enables developers of web application and services to provide a seamless single-sign-on experience for their users, easily and quickly, building on standard protocols such as OAuth, WS-Federation and SAML.

ACS’ built in support for Live, Google, Yahoo and Facebook Identities as well as the easy integration with ADFS and AD means that authentication with the most used identities is literally done with a few clicks and a little bit of configuration.

For an overview of the ACS service and a useful how-to tutorial see MSDN on – http://msdn.microsoft.com/en-us/library/gg429781.aspx

Using ACS with well-known identity providers, other than custom authentication solutions, as part of the application provides several benefits -

From the users’ perspective it prevents the need to remember a different set of credentials for the application, instead using existing identities to sign-in; this also increases security as users tend to use the same credentials for many applications, not all are good at protecting this information.

From the application’s perspective it removes some of the effort required in building scenarios such as managing credentials – storing them securely, implementing authentication functionality as well as capabilities such as reminding/resetting passwords, etc.

However – whilst integrating an application with an identity provider (or several) provides two (generally trust-worthy) facts – the knowledge that the user has been authenticated by the approved identity provider(s) and a unique identifier for that user – it does not, on its own, provide a complete end-to-end solution for authentication and authorisation; several pieces are needed on top of the ACS and IP integration beyond uniquely identifying a user, such as managing the user’s profile and implementing role based authorisation.

In this post I will be looking at the steps that are required to provide an end-to-end story for an ASP.net / MVC application using ACS with multiple identity providers to drive authentication and authorisation scenarios, I’ll start by discussing managing users’ profiles -

Managing Users’ Profiles

The Identity Providers, through ACS, will provide the application a unique identifier for the user -

The initial request from the user’s browser to the application will come as unauthenticated; at this point, given the right configuration,  Windows Identity Foundation will redirect the request to ACS which will, in turn, interact with the identity provider (as needed) before redirecting back to the application, this time with a bunch of claims regarding the details of the identity provider and the user’s identity provided through the IClaimsPrincipal object.

It does not, at this point, give you much information about the user – some identity providers, such as Google, might provide the user’s first and last name and perhaps an email address, others, such as Live ID, will only provide a unique identifier – nor does it tell you whether the user is allowed to access your application. All you know is that this is user x as declared by identity provider y.

This might be good enough for web sites that do not restrict access, and only need to know a unique id for a user, for example for personalisation purposes or to store data for a particular user, http://www.stackoverflow.com is an example for such site.

Most web sites, however, would like to, at the very least, know some basic information about the user, such as full name, perhaps date of birth or email address; some – of course – require a much more elaborate profile.

Some web sites will let anybody in, but will require updating the profile, others are membership only and so – knowing the identity of the user is one half of the story – matching it against a membership database being the other.

To support either of these scenarios, the application will need to have its own store of users’ information, linked to the identity provided through ACS.

As requests arrive from the ACS the application will need to be able to refer to this store to identify whether the user is known or new. Known users will be let in (subject to authorisation, discussed later in this article); unknown users will be, for example, directed to a registration page.

image

This really isn’t much different from how this would be implemented without the ACS- if identity was provided by ASP.net membership, for example – the main difference is that when implementing single-sign-on the identity piece and the profile management/authorisation piece are separated.

On Windows Azure, table storage is a great option for storing user’s profile – records could be stored, for example, against the identity provider (as the partition key) and user’s identity (as the row key), and given that this will generally be the only access mechanism required (I’ll be discussing a variation of that – for supporting multiple identities for the same user), it keeps the solution nice and simple.

From a technical point of view – the application needs to first pick up the user’s identity, as provided by ACS, and check that against the user’s store, and it needs to do that before running the application’s code so that the user can be considered when evaluation authorisation.

One way of achieving this is leveraging the Windows Identity Framework pipeline by implementing a custom authorisation manager by inheriting from ClaimsAuthenticationManager 

By overriding the Authenticate method you can get access to the identity provided by ACS, interrogate the claims provided with it and even make changes to the claim-set as needed.

The first step in the authenticate method would be to extract the principal as an IClaimsPrincipal –

IClaimsIdentity identity = (IClaimsIdentity)incomingPrincipal.Identity;

The next step would be to ensure that the user has actually been authenticated as this method will get called twice – once for the initial unauthenticated request, before the redirection to ACS, and once when the user is redirected back to the application from ACS with the authentication token; We’re only interested in the second call and so if the user is not authenticated we do nothing. The module’s default behaviour will take care of redirecting unauthenticated users if this was the WIF configuration.

if(identity.IsAuthenticated)
{       //code goes here
}

For authenticated users, we need to extract the claims we’re expecting from the token and ensure they exist – these are the nameidentifier and identityprovider claims

Claim id = identity.Claims.FirstOrDefault(claim => claim.ClaimType ==
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"); Claim provider = identity.Claims.FirstOrDefault(claim => claim.ClaimType ==
"http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"); if (id == null || provider == null) throw new ApplicationException(
"Reuqest did not contain the necessary authenticaiton information");

It might be obvious, but to avoid any doubt it is important to note that the user’s identity has to be composed of these two – the identity is unique in the context of the identity provider, theoretically two provides might use the same identity.

So – now that we have the user’s unique identity we can check whether it exist in our users store, this is straight forward coding against Table storage, so there’s no point repeating all the details, I have it encapsulated in two method calls

UserLineDataServiceContext context = UserLineDataServiceContext.GetContext();
UserLine user = context.FindUser(provider.Value, id.Value);

At this point user either hold the details of the user found or is null if the user has never been registered; if the user has been registered before I populate a bunch of claims specific to my application –

if (user != null) { identity.Claims.Add(new Claim(ClaimTypes.Role, "RegisteredUser")); Claim nameClaim = identity.Claims.FirstOrDefault(
                                 c => c.ClaimType == ClaimTypes.Name); if (nameClaim != null) identity.Claims.Remove(nameClaim);

      identity.Claims.Add(new Claim(ClaimTypes.Name,
                                user.FirstName + " " + user.LastName)); identity.Claims.Add(new Claim(ClaimTypes.GivenName, user.FirstName)); identity.Claims.Add(new Claim(ClaimTypes.Surname, user.LastName)); }

The first claim I populate is a role claim of a ‘RegisteredUser’, I will be using this in my application to ensure that only users with this role can access pages other than the register page as part of my authorisation implementation.

I then populate the name-related claims; this allows me to present the user’s name as given to my application in the sign-out control and other areas of my application.

Note: some IPs (such as Google) will provide you with the user’s name, others might not, in either case I allow my user to override the name with the one she wishes to use in my application, and so for registered users I need to override any claims provided by the IP.

At the end of the Authenticate method I call the base method to ensure any standard behaviour of WIF is executed –

return base.Authenticate(resourceName, incomingPrincipal);

And so – by using a few lines of code in a custom ClaimsAuthenticationManager and utilising Windows Azure Table, we’ve enabled the application to manage it’s users, distinguishing between registered and unregistered users.

The next  step would be to implement authorisation and allow unregistered users to become registered –

Role Based Authorisation and the registration page

You would have noticed the custom Claims Authorisation Manager added, for known users, the RegisterUser claim – a claim of type ‘ClaimType.Role’ – by default WIF translates claims of this type to ASP.net roles allowing familiar role based authorisation techniques to be used, in my example I’ve used this to control access to the rest of the application and to direct unknown users to the registration page.

In my case I’ve decided that the application can be accessed by any user, but it requires that users register with it application directly.

To this behaviour, preventing unregistered users access to the majority of the application, I’ve added the authorise attribute, requiring the ‘RegisteredUser’ role for access, on all my controllers other than the default controller -

[Authorize(Roles="RegisteredUser")]

By doing so, and given that this role will only be available for users that were found in the applications user’s repository, I ensure that unknown users, even if authenticated by the identity provides supported, will not be able to access any part of the application other than the home controller (which does not have this attribute)

Note: To bulletproof this approach the ClaimsAuthenticationManager should check that incoming requests do not contain the Role Claim with the text ‘RegisteredUser’ 

On the home controller I have two actions, Index and About, both are available for any user, the Index action has the following code –

public ActionResult Index()
        {
            if (!User.IsInRole("RegisteredUser"))
                return RedirectToAction("Register", "Account");
            else
            {
                return View();
            }
        }

As you can see – as users land in the default action for the application, if they are not registered they are redirected to the Register action of the Account Controller – an action that is available to any users, this action will display the registration form asking the user for details such as name and date of birth, the post action for this form looks as follows –

UserLine user = new UserLine(encodeKey(collection["IdentityProvider"]), encodeKey(collection["Identity"]));
            user.FirstName = collection["FirstName"];
            user.LastName = collection["LastName"];
            user.EmailAddress = collection["EmailAddress"];

            UserLineDataServiceContext context = UserLineDataServiceContext.GetContext();
            context.AddUser(user);
            ClaimsIdentity identity = User.Identity as ClaimsIdentity;
            
            identity.Claims.Add(new Claim(ClaimTypes.Role,"RegisteredUser"));
            //add name claims according to registration information
            identity.Claims.Add(new Claim(ClaimTypes.Name, user.FirstName + " " + user.LastName));
            identity.Claims.Add(new Claim(ClaimTypes.GivenName, user.FirstName));
            identity.Claims.Add(new Claim(ClaimTypes.Surname, user.LastName));

            return RedirectToAction("Index", "Home");

Admittedly not the most robust code in the world, but good enough as a sample it creates a new UserLine, populating it with the information from the form and adds it to the Table before adding all the necessary claims for this user.

These claims, including the RegistredUser role claim would normally be added by the ClaimsAuthenticationManager but in this case they are now added in this form to allow the user to be treated as a recognised user by the application.

With these set the user can be redirected back to the default action, this time with the correct role which would allow the default view to be returned.

Supporting Multiple Identities for same user

Everything that discussed so far assumes the use is only identified using one identity provider and whilst this is a fair assumption for some web sites, most of those who wish to support identity federation want to support more than one provider and to make user’s life as convenient as possible it is important to be able to recognise users using more than one identity provider.

Given that there’s no way for any one IP. or the ACS, to link identities, this is up for the application, or – more accurately- up for the application to allow the user to do so.

I haven’t fully implemented this for my sample, but the approach would be to allow the user, in the registration page, to indicate that she is already known using a different identity and then to be able to provide a token for this identity (through ACS, of course).

The key to this is to expand the user’s repository – every user should be given an ‘internal identity’ – managed by the application and separate table will link any IP-provided identity to the relevant internal identity, any records in the application should always be stored against the internal identity.

Summary

I hope that through this post I was able to demonstrate that whilst enabling ACS for an application is only a first step towards achieving a full end-to-end authentication and authorisation solution for an application, the steps required to complete the solutions are quite straight forward and lean, and that’s the whole point in identity federation – to take away the majority of the work needed, whilst leaving a good level of control in the application.

About these ads

About Yossi Dahan
I work as a principal consultant in the CTO office of Solidsoft - a Microsoft partner in the UK with a strong focus on cloud, hybrid and integration based solutions. I spend my days working with both our customers and our project teams, helping them explore the possibilities that technology enables and how to derive value from them.

5 Responses to End-to-end authentication and authorisation scenario for MVC+ACS

  1. Anthony says:

    Yossi…Thanks for this article, i am new to Azure ACS and was trying to figure out how to do this. You explained it in a clear understandable way.

  2. John says:

    This is fantastic, thanks a ton. Helped close the circle on the ‘next steps’ of ACS & WIF.

  3. DotNetWise says:

    Supporting Multiple Identities for same user
    That would be the key, really – no real examples on the internet yet :(

    • EShy (@EShy) says:

      The new web app template in VS 2012 RTM is supposed to have an example of multiple identities for the same user (check Hanselman’s keynote from aspconf.net)

      If you want to implement it yourself, all you need to do really is a many to many between the nameidentifier and your accounts instead of a one to one

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: