views:

979

answers:

3

I'm currently investigating alternative solutions to the standard page authorization configuration in asp.net.

The location tag works ok if you have directories of files that need the same access policy but if you have lots of individual access policies the location tag is a pain. I could roll my own custom auth system but if it can be avoided thats probably better.

Currently we're using a azman-like permission based authorization system for page content but I haven't found a good way of integrating this with the standard page security yet.

Any suggestions on how to do this? Are there any solutions integrating azman and asp.net page authorization? Are there any other standard solutions I should be aware of?

A: 

Have you setup the GenericIdentity and IPrincipal objects during your Application_AuthenticateRequest method of the application?

We currently use our domain to do authentication and user groups/roles on the SQL Server database to provide authorization. During the Application_AuthenticateRequest method, I gather all this data and create a FormsAuthenticationTicket object based on it.

By doing so, I now have access to the user's roles by performing a simple User.IsInRole("RoleX") command in my code, which allows me to easily lock/unlock user controls, or even do a simple Response.Redirect() to a "Authorization Error" page if they don't have the proper authorization.

Here's what my AuthenticateRequest method looks like (VB.NET)

Sub Application_AuthenticateRequest(ByVal sender As Object, _
                                       ByVal e As EventArgs)

      Dim formsAuthTicket As FormsAuthenticationTicket
      Dim httpCook As HttpCookie
      Dim objGenericIdentity As GenericIdentity
      Dim objMyAppPrincipal As CustomPrincipal
      Dim strRoles As String()

      httpCook = Context.Request.Cookies.Get("authCookieEAF")
      formsAuthTicket = FormsAuthentication.Decrypt(httpCook.Value)
      objGenericIdentity = New GenericIdentity(formsAuthTicket.Name)
      strRoles = formsAuthTicket.UserData.Split("|"c)
      objMyAppPrincipal = New CustomPrincipal(objGenericIdentity, strRoles)
      HttpContext.Current.User = objMyAppPrincipal    

   End Sub

...and similarly, here's what the CustomPrincipal object looks like:

Public Class CustomPrincipal
   Implements IPrincipal


   ''' <summary>
   '''    Identity object of user.
   ''' </summary>
   ''' <remarks></remarks>
   Private m_identity As IIdentity

   ''' <summary>
   '''    Roles(s) a user is a part of.
   ''' </summary>
   ''' <remarks></remarks>
   Private m_roles As String()

   ''' <summary>
   '''    Name of user.
   ''' </summary>
   ''' <remarks></remarks>
   Private m_userId As String

   ''' <summary>
   '''    Gets/Sets the user name.
   ''' </summary>
   ''' <value>m_userId</value>
   ''' <returns>Current name of user.</returns>
   ''' <remarks></remarks>
   Public Property UserId() As String
      Get
         Return m_userId
      End Get
      Set(ByVal value As String)
         m_userId = value
      End Set
   End Property

   ''' <summary>
   '''    Gets the identity object of the user.
   ''' </summary>
   ''' <value>m_identity</value>
   ''' <returns>Current identity of user.</returns>
   ''' <remarks></remarks>
   Public ReadOnly Property Identity() As System.Security.Principal.IIdentity Implements System.Security.Principal.IPrincipal.Identity
      Get
         Return m_identity
      End Get
   End Property

   ''' <summary>
   '''    Full constructor.
   ''' </summary>
   ''' <param name="identity">Identity to use with Custom Principal.</param>
   ''' <param name="roles">Roles for user.</param>
   ''' <remarks>Identity object contains user name when building constructor.</remarks>
   Public Sub New(ByVal identity As IIdentity, ByVal roles As String())

      m_identity = identity
      m_roles = New String(roles.Length) {}
      roles.CopyTo(m_roles, 0)
      Array.Sort(m_roles)
      m_userId = identity.Name

   End Sub

   ''' <summary>
   '''    Determines if the current user is in the role specified.
   ''' </summary>
   ''' <param name="role">Role to test against.</param>
   ''' <returns>Boolean variable indicating if role specified exists in user's m_roles array.</returns>
   ''' <remarks></remarks>
   Public Function IsInRole(ByVal role As String) As Boolean Implements System.Security.Principal.IPrincipal.IsInRole

      Dim boolResults As Boolean

      If Array.BinarySearch(m_roles, role) >= 0 Then
         boolResults = True
      Else
         boolResults = False
      End If

      Return boolResults

   End Function

End Class

Hopefully this gives you what you need to mold it into your environment.

Dillie-O
We have the same in our applications thaugh we just use a custom roleprovider and membership provider for authentication and role loading.What I really want to know is, do you then hard-code a User.IsInRole("Admin") as the first thing in page_init on each of your admin pages. Or do you have some other way of decoupling this configuration? I wouldn't like to hard-code role or azman rule names into our pages.
JohannesH
I use a combination of web.config authorization and master templates to keep my actual code checking down to a minimum. I also have a couple of helper functions like "IsAdminAuthorized" and "IsManagerAuthorized" that do the composite checks when users in several different roles qualify as an admin or a manager.
Dillie-O
Ok, to me that seems a little too coupled. I don't like hard coding. Basically I think I need to make something like the UrlAuthorization module.
JohannesH
+2  A: 

I did in a huge application having lots of different permissions and different roles something like the following [I don't have the code here so I'll just try to recreate it here]:

I first implemented a class called SecuredPage as following:


public class SecuredPage : System.Web.UI.Page
{
    // Those Permissions are mandatory, so user needs to have all of them
    public List MandatoryPermissions { get; set; }

    // Those Permissions are optional, so if the user have at least one of them, he can access
    public List OptionalPermissions { get; set; }

    protected override void OnLoad(EventArgs e)
    {
     MyUser loggedUser = (MyUser) this.User;

     base.OnLoad(e);

     foreach (Permission mandatoryPermission in MandatoryPermissions)
     {
      // if the user don't have permission, we can redirect him
      if (!loggedUser.HasPermission(mandatoryPermission))
      {
       RedirectToDontHaveAccess();
       break;
      }
     }

     bool hasAccessToThePage = false;

     foreach (Permission optionalPermission in OptionalPermissions)
     {
      // If the user has at least one of the permissions, he can access
      if (loggedUser.HasPermission(optionalPermission))
      {
       hasAccessToThePage = true;
      }
     }

     if (!hasAccessToThePage)
     {
      RedirectToDontHaveAccess();
     }

    }

    private void RedirectToDontHaveAccess()
    {
     throw new NotImplementedException();
    }
}

This will be my BasePage for all pages that user need permissions to access. The MandatoryPermissions are permissions that user MUST have all of them to access the page and OptionalPermissions are permissions that user needs at least one of them to access the page. There's no need to use both on every page because if you have MandatoryPermissions doesn't matter if you have the optionals or not.

Permission is a enum:


public enum Permission
{
    // Usually this enum will replicate a domain table from the database
    EditUser = 1,
    SearchUserByUsername = 2,
    SearchUserByEmail = 3

}

And MyUser is a implementation of MembershipUser:


public class MyUser : System.Web.Security.MembershipUser
{
    internal bool HasPermission(Permission permission)
    {
     //
     // TODO: Check on database if the user has the permission or not
     //
    }
}

Then the only thing that you need to do in your pages is to populate the permissions lists:


public partial class EditUser : SecuredPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
     MandatoryPermissions.Add(Permission.EditUser);
    }
}

public partial class SearchUser : SecuredPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
     OptionalPermissions.Add(Permission.SearchUserByUsername);
     OptionalPermissions.Add(Permission.SearchUserByEmail);
    }
}

OK, the search example wasn't that good but I think you get the picture.

The whole idea is that base.OnLoad(e); is called just before the permissions verification, so you just need to fill the permissions in your Page_Load.

I'm not sure if this is the best solution but I'm sure it helps a lot :)

homemdelata
I have implemented a MembershipProvider, and a RoleProvider. My questions is how you decouple the authorization from the individual pages. I mean, do you put a Roles.IsInRole("admin") in each of your pages? Do you use web.config location elements? Or do you have another way of decoupling it from the pages?
JohannesH
Ah, ok, So I'll edit my answer to show how I usually do :) Just a second
homemdelata
Yep, the "base page" type approach is what we do too - Have a base page that handles all the authentication/access control, etc, and a few other functions, and then inherit the individual pages from that.
Zhaph - Ben Duguid
+1  A: 

How about mapping pages to roles in your database and then let your masterpage check the DB on pageload?