4

I'm trying to do what should be a simple thing in MVC3.

I've got an application that uses forms authentication to authenticate users with a 3rd party SSO. The SSO, on successful login, posts back to a specific controller action on my application. I then call FormsAuthentication.SetAuthCookie(user,false);.

I'm trying to implement some level of authorization. Simply, a user can exist in a number of different roles, e.g. Admin and Developer. Some controller actions should only be available to certain roles. Details of which roles a user belongs to is obtained by making a call to another external API, which returns a simple JSON response indicating.

In theory, this should be as simple as doing something like this after I set the FormsAuthentication cookie:

string[] rolelist = GetRoleListForUserFromAPI(User.Identity.Name);
HttpContext.User = new GenericPrincipal(User.Identity, rolelist);

However, I can't call this directly after calling SetAuthCookie, because HttpContext.User isn't anything meaningful at this point.

I could try setting this on every request, but ever request to my app would mean a roundtrip API call.

The most promising approach I've seen so far is to create a custom Authorization attribute and override OnAuthorization to do something like this:

public override void OnAuthorization(AuthorizationContext filterContext)
{
    if (<some way of checking if roles have already been set for this user, or role cache has timed out>)
    {
        string[] rolelist = GetRoleListForUserFromAPI(filterContext.HttpContext.User.Identity.Name);
        filterContext.HttpContext.User = new GenericPrincipal(filterContext.HttpContext.User.Identity,rolelist);
    }
}

I could then use [MyCustomAuthorization(Roles="Admin")] in front of controller actions to make the magic happen.

However, I've no idea how to detect whether or not the current HttpContext.User object has had its roles set, or whether it was set over a certain time ago and another API trip is needed.

What's the best approach for this?

growse
  • 3,554
  • 9
  • 43
  • 66

4 Answers4

2

You should override PostAuthenticateRequest

protected void Application_OnPostAuthenticateRequest(object sender, EventArgs e) 
{
    if (HttpContext.Current.User.Identity.IsAuthenticated)
    {
        string[] rolelist = GetRoleListForUserFromAPI(User.Identity.Name);
        HttpContext.User = new GenericPrincipal(User.Identity, rolelist);
    }
}

It's invoked after forms authentication is finished with it's processing.

http://msdn.microsoft.com/en-us/library/ff647070.aspx

Update

I had the wrong method signature (just checked in one of my own applications).

jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • Brb, off to do some testing on this :) – growse Jun 08 '12 at 12:55
  • @jgauffin - this looks interesting, but I'm wondering how this information get's persisted on subsequent requests. – ek_ny Jun 08 '12 at 12:58
  • Looks like it's persistent here, putting `IsAdmin: @User.Identity.IsInRole("Admin")` on the `Master.cshtml` is saying `True` on each page. – growse Jun 08 '12 at 12:59
  • @ek_ny: Authentication is never stored. It's always loaded for each request. You could go about and use the session in PostAuthenticate if you want. – jgauffin Jun 08 '12 at 13:03
  • Yes, my issue with this is that it's being called on every single request, including requests to things like javascript files and images. – growse Jun 08 '12 at 13:12
  • There is now way around that. It's the proper place to put authentication since it has to be done before any authorization. It may seem like a waste of resources but isn't very heavy. You could of course use the session to cache the roles. – jgauffin Jun 08 '12 at 13:18
  • Do you know if the session is specific to a particular app, or if two apps share the same MachineKey, will they also be able to read each other's session – growse Jun 08 '12 at 15:15
  • sorry, don't know. ask a new question =) – jgauffin Jun 08 '12 at 17:48
2

Another way would be to store the roles in the UserData property of the FormsAuthentcationTicket. This could be done with comma delimited string.

http://msdn.microsoft.com/en-us/library/system.web.security.formsauthenticationticket.formsauthenticationticket

Then on AuthenticateRequest method, you could pull the ticket back, grab the roles data and assign it to the current user using a generic principal.

ek_ny
  • 10,153
  • 6
  • 47
  • 60
  • This is a good idea - I could feasibly store a caching timout value, in case I want to refresh the role list more than once during the user session (say, every 5 minutes). – growse Jun 08 '12 at 13:00
1

My first thought is that you should investigate implementing a custom role provider. This might be overkill but seems to fit in with the role-based plumbing.

More info from MSDN here.

stephen.vakil
  • 3,492
  • 1
  • 18
  • 23
  • This isn't actually a bad approach, although daunting as it might seem at first. It was fairly simple to create a class, inherit from RoleProvider and implement a couple of the methods to call the API to get role information. Some simple caching in the middle and it seems to work rather well. Added bonus is that I can pull into a separate dll and distribute via nuget to all apps that want to authorize against this particular API. – growse Jun 08 '12 at 21:28
1

Much to the aghast of some, the session object ISNT a bad idea here. If you use temp data, you already take a hit for the session.

Storing this data in the cookie, well - Forms auth tokens have already been exploited in the POET vulnerability from a year and a half ago, so in that case someone could've simply formed their own cookie with the "admin" string in it using that vulnerability.

You can do this in post authenticate as @jgauffin mentioned. If the session state isn't available there you can use it then in Application_PreRequestHandlerExecute and check it there.

If you want to check if session state is available in either see my code at: How can I handle forms authentication timeout exceptions in ASP.NET?

Also whenever using forms auth and sessions, you always want to make sure the timeouts are in sync with each other (again the above code)

Community
  • 1
  • 1
Adam Tuliper
  • 29,982
  • 4
  • 53
  • 71