Pages

Wednesday, April 30, 2014

Custom Role Provider

I'll be showing you how I recently implemented a Custom Role Provider in .NET for security within an MVC application.

We were using Claims-Based Authentication via ADFS.  These technologies were a good fit because internal users are Authenticating via their domain account and Active Directory, and external users were Authenticating via SAML 2.0 token from 3rd party Identity Provider.  We configured ADFS to take a Claims from both interfaces.  Click this link to read more about Claims-Based Single Sign-On

The SXP (from my previous post) is integrated with the MVC application and has all the code to get roles from Authorization Web Service and set in custom role provider.  You do not need an SXP to do this, this can all be done in just an MVC project.

You need to implement System.Web.Security.RoleProvider abstract class.  There are numerous methods in here, but I'm only going to write code for 2 of them (I put my class under Model\Security\ in my MVC app)  I'm leaving the rest as not implemented because I didn't need them.   

     public class ComponentRoleProvider : RoleProvider
    {
        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }
        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }
        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }
        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }
        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }
        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }
        public override string[] GetRolesForUser(string username)
        {
            return SessionManager.User.UserComponentPrivileges.Select(ucp => ucp.ComponentName).ToArray();
        }
        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }
        public override bool IsUserInRole(string username, string roleName)
        {
            return this.GetRolesForUser(username).Contains(roleName);
        }
        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }
        public override bool RoleExists(string roleName)
        {
            throw new NotImplementedException();
        }
    }

 The work flow for this to happen is in Session_Start event in Global.asax file, there is an initialization of a users session.  This initialization reads the claim from ADFS that is attached to identity on the current thread.

var identity = Thread.CurrentPrincipal.Identity as ClaimsIdentity;
            if (identity.AuthenticationType == "Federation")
            {
                List<string> ADGroups = new List<string>();
                foreach (System.Security.Claims.Claim curClaim in identity.Claims)
                {
                    Logger.Log("Claim.Type: " + curClaim.Type + " | " + curClaim.Value, TraceLevel.Verbose);
                    switch (curClaim.Type)
                    {
                        case ClaimTypes.Email:
                            claim.Email = curClaim.Value;
                            break;
                        case ClaimTypes.Name:
                            claim.Username = curClaim.Value;
                            // set the issuer and original issuer when we get the username. 
                            // original issuer will come from any 3rd party claims provider
                            claim.Issuer = curClaim.Issuer;
                            claim.OriginalIssuer = curClaim.OriginalIssuer;
                            break;
                        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname":
                            claim.FirstName = curClaim.Value;
                            break;
                        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname":
                            claim.LastName = curClaim.Value;
                            break;
                        case "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod":
                            claim.AuthMethod = curClaim.Value;
                            break;
                        case "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant":
                            claim.AuthTime = DateTime.Parse(curClaim.Value);
                            break;
                        case "http://schemas.xmlsoap.org/claims/Group":
                            // we actually want to be using group SIDS so group names may change
                            //ADGroups.Add(curClaim.Value);
                            break;
                        case "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid":
                            ADGroups.Add(curClaim.Value);
                            break;
                        case "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid":
                            claim.UserSID = curClaim.Value;
                            break;
                    }
                }
                claim.ADGroups = ADGroups.ToArray();
            }
            else
            {
                claim.Username = identity.Name;
            }


Once we have the claim values we can call our Authorization web service with unique id, in this case its emails address, and get the components user has access to.  After a user object (PortalUserDTO) is return from Authorization web service we save that object in Session (using static SessionManger in my example).

             using(AuthorizationServiceClient client = new AuthorizationServiceClient())
            {
                try
                {
                    PortalUserDTO portalUser = client.GetPortalUser(claim, applicationName);
                    SessionManager.User = portalUser;
                    client.Close();
                }
                catch(Exception ex)
                {
                    client.Abort();
                    throw ex;
                }
            }

Our database is setup in the following manner:
  • Components (resources that you want to define security around)
  • ApplicationComponents (linking up Components with a specific application)
  • ApplicationComponentsPortalRolePermission (defining role that has access to ApplicationComponent and permission level read/write)
  • PortalRoles (Roles in system that users get associated with)
  • PortalUser (Users in the system)
  • PoralUserRoles (Role a user has access to)
We have a view that neatly roles up all these tables into a linear distinct result set.  When view is filtered by PortalUserId we get distinct components that user has access to.  This distinct set of components for a user is kept in the SessionManager.User.UserComponentPrivileges collection. 

From the code above for ComponentRoleProvider.GetRolesForUser method we use LINQ to select the component name for each UserComponetPrivilege.  That value can new be applied in your controllers at the class or method level.  A user must have access to that component to access the methods in the controller.

    [Authorize(Roles = "ComponentName")]
    public class MyController : Controller



No comments:

Post a Comment