Pergunta

I'm using forms authentication in my MVC application. This is working fine. But not I want to adjust authorization to only allow people in certain roles. The logins correspond to users in active directory and the roles correspond to the groups the users are in.

For authentication, I simply call FormsAuthentication.SetAuthCookie(username, true) after verifying the login.

For authorizing, I first applied the attribute to the controllers I want to secure

[Authorize(Roles = "AllowedUsers")]
public class MyController
...

Next, I'm handling the OnAuthenticate event in global.asax.

protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs args)
{
    if (FormsAuthentication.CookiesSupported)
        {
            if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
            {
                try
                {
                    FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(
                    Request.Cookies[FormsAuthentication.FormsCookieName].Value);

                    // Create WindowsPrincipal from username.  This adds active directory
                    // group memberships as roles to the user.
                    args.User = new WindowsPrincipal(new WindowsIdentity(ticket.Name));

                    FormsAuthentication.SetAuthCookie(ticket.Name, true);
                }
                catch (Exception e)
                {
                    // Decrypt method failed.
                }
        }
    }
    else
    {
        throw new HttpException("Cookieless Forms Authentication is not " + "supported for this application.");
    }
}

With this when someone accesses the website they get the login screen. From there they can actually log in. However, somehow it doesn't save the auth cookie and they get a login screen after the next link they click. I tried adding a call to SetAuthCookie() in OnAuthenticate() but they made no difference.

Before I added this event handler to handle authorization, authentication worked fine. So somewhere in the framework User is being set. I'm wondering if this the correct approach and I'm just missing something or if I need a different approach.

What do I need to do to get this to work?

Thanks, Scott

Foi útil?

Solução

It seems like my initial approach won't work. I was trying to get ASP.NET to automatically load user roles from their AD account. No comment was given on whether this was possible. However, the research I've done indicates I'll have to write code to load AD group memberships into user roles.

The solution to creating the user principal that ASP.NET MVC uses appears to be to create it in FormsAuthentication_OnAuthenticate() and assign it to Context.User. It appears if I don't set Context.User ASP.NET MVC creates a user principal based off the auth ticket after FormsAuthentication_OnAuthenticate() returns. Additionally, ASP.NET MVC appears to do nothing with Context.User if I set it in FormsAuthentication_OnAuthenticate().

The following is what I ended up doing.

This is the code that handles authentication

public ActionResult LogOn(FormCollection collection, string returnUrl)
{
    // Code that authenticates user against active directory
    if (authenticated)
    {
        var authTicket = new FormsAuthenticationTicket(username, true, 20);

        string encryptedTicket = FormsAuthentication.Encrypt(authTicket);

        var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
        authCookie.Expires = DateTime.Now.AddMinutes(30);
        Response.Cookies.Add(authCookie);

        if (Url.IsLocalUrl(returnUrl)
            && returnUrl.Length > 1
            && returnUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase)
            && !returnUrl.StartsWith("//", StringComparison.OrdinalIgnoreCase)
            && !returnUrl.StartsWith("/\\", StringComparison.OrdinalIgnoreCase))
        {
            return Redirect(returnUrl);
        }
        else
        {
            return Redirect("~/");
        }
   }
   return View();
}

I initially tried just calling FormsAuthentication.SetAuthCookie(username, true) instead of manually creating, encrypting, and adding it to the Response cookie collections. That worked in the development environment. However, it didn't after I published to the website.

This is the log off code

public ActionResult LogOff()
{
    var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {
        authCookie.Expires = DateTime.Today.AddDays(-1);
    }

    Response.Cookies.Add(authCookie);
    FormsAuthentication.SignOut();

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

FormsAuthentication.SignOut() doesn't seem to do anything after I switched to manually creating, encrypting, and adding the auth ticket to the response cookie collection in the logon code. So I had to manually expire the cookie.

This is the code I have for FormsAuthentication_OnAuthenticate()

protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs args)
{
    HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie == null || string.IsNullOrWhiteSpace(authCookie.Value))
        return;

    FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);

    UserData userData = null;
    if (Application["UserData_" + authTicket.Name] == null)
    {
        userData = new UserData(authTicket.Name);
        Application["UserData_" + authTicket.Name] = userData;
    }
    else
    {
        userData = (UserData)Application["UserData_" + authTicket.Name];
    }

    Context.User = new GenericPrincipal(new GenericIdentity(authTicket.Name), userData.Roles);
}

UserData is a class I created to handle caching user roles. This was needed because of the time it takes for active directory to return the group memberships the user belongs to. For completeness, the following is the code I have for UserData.

public class UserData
{
    private int _TimeoutInMinutes;
    private string[] _Roles = null;

    public string UserName { get; private set; }
    public DateTime Expires { get; private set; }
    public bool Expired { get { return Expires < DateTime.Now; } }
    public string[] Roles
    {
        get
        {
            if (Expired || _Roles == null)
            {
                _Roles = GetADContainingGroups(UserName).ToArray();
                Expires = DateTime.Now.AddMinutes(_TimeoutInMinutes);
            }
            return _Roles;
        }
    }

    public UserData(string userName, int timeoutInMinutes = 20)
    {
        UserName = userName;
        _TimeoutInMinutes = timeoutInMinutes;
    }
}

Outras dicas

Roles can also be stored in a cookie and you have at least two options:

  • a role provider cookie (another cookie that supports the forms cookie), set with cacheRolesInCookie="true" on a role provider config in web.config. Roles are read the first time authorization module asks for roles and the cookie is issued then

  • a custom role provider that stores roles in the userdata section of the forms cookie, roles have to be added to the user data section of the forms cookie manually

The Authorization module asks the current principal for user roles, which, if role provider is enabled, either scans the role cookie (the first option) or fires the custom role provider methods.

Yet another, recommended approach is to switch to the Session Authentication Module (SAM) that can replace forms authentication. There are important pros, including the fact that SAM recreates ClaimsPrincipal out of the cookie and roles are just Role claims:

// create cookie

SessionAuthenticationModule sam = 
   (SessionAuthenticationModule)
   this.Context.ApplicationInstance.Modules["SessionAuthenticationModule"];

ClaimsPrincipal principal = 
   new ClaimsPrincipal( new GenericPrincipal( new GenericIdentity( "username" ), null ) );

// create any userdata you want. by creating custom types of claims you can have
// an arbitrary number of your own types of custom data
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Role, "role1" ) );
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Role, "role2" ) );

var token = 
    sam.CreateSessionSecurityToken( 
        principal, null, DateTime.Now, DateTime.Now.AddMinutes( 20 ), false );
sam.WriteSessionTokenToCookie( token );

From now on, the identity is stored in a cookie and managed automatically and, yes, the Authorization attribute on your controllers works as expected.

Read more on replacing forms module with SAM on my blog:

http://www.wiktorzychla.com/2012/09/forms-authentication-revisited.html

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top