RSS

Custom Security OData Service – Wcf Data Services

14 Jun

I ran into an authentication/authorization make-a-decision-issue, about how to provide custom access to an OData Service. Also, I wanted this to work through “single” permissions for each user. What I mean by this is not to give users the final permissions through roles, but more directly, so I could authorize users consuming  the service  in a more flexible manner.
For the authentication-part: There are many ways to do this.  Please read an interesting article from Mike Taulty on this topic.

Authentication
Fact: You could use asp.net form based authentication, windows authentication etc.. This for me wasn’t an option, because of the service’s “open” character. One would bother customers having a complex implementation time; assembling cookies, firing exotic client tools or spent hours on learning the-microsoft-way when they’re not used to it (which isn’t a bad thing of course, but let’s respect our fellow developers :-) ) Although, I wanted to leave the MembershipProvider -’pattern’ in, cause this comes in very handy.

I descided to go for a SSL/Basic Authentication solution, which is absolute secure!! (There are other discussions on this, which I leave out for now). Let’s talk about Basic Authentication for one minute:

On an Http-Request, Before transmission, the user name is appended with a colon and concatenated with the password. The resulting string is encoded with the Base64 algorithm. For example, given the user name Aladdin and password open sesame, the string Aladdin:open sesame is Base64 encoded, resulting in QWxhZGRpbjpvcGVuIHNlc2FtZQ==. The Base64-encoded string is transmitted and decoded by the receiver, resulting in the colon-separated user name and password string.

First thing we have to do is design some kind of a mechanism to handle requests containing these credentials and authenticate our user through a custom MembershipProvider.
Let’s start by creating a new class implementing the IHttpModule:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Security.Principal;
using NL.ADA.TNT.CustomService.Modules;
using Ada.Cdf.Logging;

namespace NL.ADA.TNT.CustomService.Modules
{
    public class ODataServiceModule : IHttpModule
    {

        private CustomMembershipProvider membershipProvider;
        private Logger _logger;

        public ODataServiceModule()
        {
            _logger = new Logger();
        }

        #region IHttpModule Members

        public void Dispose()
        {
            _logger.Log(LogEvent.EnterEvent);
            membershipProvider = null;
            _logger.Log(LogEvent.LeaveEvent);
        }

        public void Init(HttpApplication
{
            membershipProvider = (CustomMembershipProvider)Membership.Provider;
            context.AuthenticateRequest += new EventHandler(context_AuthenticateRequest);
            context.AuthorizeRequest += new EventHandler(context_AuthorizeRequest);
            context.BeginRequest += new EventHandler(context_BeginRequest);
        }

        void context_BeginRequest(object sender, EventArgs e)
        {
                   HttpApplication context = sender as HttpApplication;
            if (context.User == null)
            {
                if (!TryAuthenticate(context))
                {
                    SendAuthHeader(context);
                    return;
                }

            }

        }

        void context_AuthorizeRequest(object sender, EventArgs e)
        {
		//insert custom code
        }

        void context_AuthenticateRequest(object sender, EventArgs e)
        {

            //TODO: rules?
            HttpApplication context = sender as HttpApplication;
            TryAuthenticate(context);

        }

        #endregion

        private void SendAuthHeader(HttpApplication context)
        {
            context.Response.Clear();
            context.Response.StatusCode = 401;
            context.Response.StatusDescription = "Unauthorized";
            context.Response.AddHeader("WWW-Authenticate", "Basic realm=\"Secure Area\"");
            context.Response.Write("401 please authenticate");
            context.Response.End();

        }

        private bool TryAuthenticate(HttpApplication context)
        {

            string authHeader = context.Request.Headers["Authorization"];

            if (!string.IsNullOrEmpty(authHeader))
            {
                if (authHeader.StartsWith("basic ", StringComparison.InvariantCultureIgnoreCase))
                {

                    string userNameAndPassword = Encoding.Default.GetString(
                        Convert.FromBase64String(authHeader.Substring(6)));
                    string[] parts = userNameAndPassword.Split(':');

                    if (membershipProvider.ValidateUser(parts[0], parts[1]))
                    {
                        var userIdentity = new GenericIdentity(parts[0].Trim(), "Basic");
                        context.Context.User = new CustomServicePrincipal(userIdentity);

                        return true;
                    }
               }
            }

            return false;
        }
    }
}

As you can see, we create a new Principal in this line:

context.Context.User = new CustomServicePrincipal(userIdentity);

I’ll come back to this in a minute. First you’ll have to notice this is a custom module that enables you to handle basic-authentication in IIS (7.5).
Next we’ve to register this as a custom module in IIS…

Add Custom Module In IIS

….and disable all authentication-options, except anonymous:

disable authentication iis

For now our module is set and intercepts each request doing our custom thing.

Authorization

As I mentioned earlier, for the actual authorization purpose we create a custom principal. that is a new Class that inherits GenericPrincipal. By doing this we’re able to “write” our user to the HttpContext and can call it’s UserPermissionSet property anytime:

   public class CustomServicePrincipal : GenericPrincipal
    {
        private IIdentity _identity;
        private MyEntities _context;

        public CustomServicePrincipal(IIdentity identity)
            : base(identity, new string[] { })
        {

            _context = new MyEntities();
            _identity = identity;
            UserPermissionSet = GetUserPermissionSet();
        }

        public Permission UserPermissionSet { get; set; }
        private Permission GetUserPermissionSet()
        {
            try
            {
                var query = from up in _context.UserPermissions
                            where up.User.UserName == this.Identity.Name
                            select up.Permission;

                Permission flags = 0;
                foreach (var userPermission in query)
                {
                    flags |= (Permission)userPermission.ID;
                }
                return flags;

            }
            catch (Exception ex)
            {

                throw new Exception(string.Format("could not get permissionset for current user{0}", this.Identity.Name), ex.InnerException);
            }

        }

     }

Please note: the permissionset is received from the database (thru entity-framework 4.0). If you want to authorize methods, properties and so on, the thing that worked for me is to handle those permissions as bitwise “flags” so that later on you can compare very easily throughout all the application. To make this clear, here’s an example:

   [Flags]
    public enum Permission
    {
        userMayReadBedtimeStories = 1,
        userMayReadHorrorStories = 2,
        userMayReadHumoristicStories = 4,
        userMayReadNewsStories = 8,
        userMayReadOtherKindoStories = 16,
        userMayPostStory = 32

    }

 

At the persistance-level I stored these permissions in a “permission”-table which relates to table “User”. Username and Password are stored here also. Permissions for each user we’ll get from the “UserPermission”-crosstable.

Now we’re ready to demand some of these permissions on a method:

        [ChangeInterceptor("Stories")]
        public void OnChangeStory(Story story, UpdateOperations updateOperations)
        {
                      AuthDemand(Permission.userMayPostStory);
//add a story
.
.
.
}
        private void AuthDemand(Permission demandPermissions)
        {
            var userPermissionSet = (HttpContext.Current.User as CustomServicePrincipal).UserPermissionSet;
            if ((userPermissionSet & demandPermissions) != (demandPermissions))
            {
                throw new Exception("no permission");//put whatever you want to throw here
            }
        }

thank you also:
Bjorns Blog

Advertisement
 

About franssenden

I'm a developer at ADA-ICT in The Netherlands.
2 Comments

Posted by on June 14, 2010 in Uncategorized

 

Tags: , , , ,

2 Responses to Custom Security OData Service – Wcf Data Services

  1. Daniel Skowroński

    August 17, 2010 at 2:15 pm

    Great,

    but two things:

    In public void Init(HttpApplication) how can you access Membership.Provider, I don’t have here that kind of property. I have ASP.NET MVC 2.0 App that contains logic.

    Where is implementation of CustomMembershipProvider?

    Can you add so it could be understood what is going on when you call if (membershipProvider.ValidateUser(parts[0], parts[1]))

    Regards,
    Daniel Skowroński

     
    • franssenden

      August 17, 2010 at 11:41 pm

      Hi Daniel,

      Use the ‘System.Web.Security.Membership’. I suppose it will work also for asp.net mvc.
      Here’s an article: http://dotnetaddict.dotnetdevelopersjournal.com/aspnet35_membership.htm

      You’ll have to inherit from ‘System.Web.Security.Membership’ to create your own.
      An example of implementation:
      public override bool ValidateUser(string username, string password)
      {
      _logger.Log(LogEvent.EnterEvent);
      Site site;
      var query = from s in _context.Sites
      where s.UserName == username
      select s;
      if (query.Count() > 0)
      {
      site = query.First();

      if (CheckPassword(password, site.Password))
      return true;
      }

      _logger.Log(LogEvent.InfoEvent, string.Format(“passwordcheck failed for user {0}”, username));
      return false;
      }

      Hope this will help. Let me know!
      Best, Frans.

       

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 )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.