none
Method-level authorization in a WCF REST service? RRS feed

  • Question

  • Hi,

    I'm pretty frustrated with this right now and can't figure it out.  I have a simple WCF REST service set up, with the help of the WCF REST starter kit.  My service is using the WebServiceHost2 class.  Now, I know you could authenticate and authorize a user on a per-method basis by sticking some code at the top of every method, but I'd like a more robust way to do it.  Is there some way I can intercept the call to the REST method on its way, and authenticate/authorize the user to check whether they have permissions to access that method before the method is actually called?  I'd probably want access to the method's MethodInfo too so I could tag each one with a custom attribute, giving a list of roles that are authorized to call that method.

    Thanks!
    === Jez ===
    Friday, August 14, 2009 9:48 AM

Answers

  • Carlos,

    Not quite what I needed, but thanks, because your code led me to the solution I did need, eventually.  :-)

    The thing is, your code means that, to apply the security check to a given operation, you need to remember to apply the 'MyParamInspector' attribute to that operation.  So, by default, the operation is allowed as that attribute isn't applied and the security code doesn't run.  I wanted to 'punish' the developer for forgetting to apply such an attribute, by defaulting to 'no access allowed'.  What I did end up doing, though, was applying my IOperationBehavior/IParameterInspector class to EVERY operation, automatically, programatically.  This is done in my web service host, which extends WebServiceHost2, and the IOperationBehavior/IParameterInspector class is applied in the OnOpening() method, like so:

    // Attach authorization code to each and every operation
    foreach (var ep in this.Description.Endpoints) {
    	foreach (var op in ep.Contract.Operations) {
    		op.Behaviors.Add(new MyOperationAuthorizer());
    	}
    }

    The actual authorizer itself then checks to see whether the method has had my necessary custom roles attribute applied to it, and if not, automatically denies access to the operation.  If it has, it can then check to see whether the user is in one of the necessary roles:

    public class MyOperationAuthorizer : IOperationBehavior, IParameterInspector {
    	/// <summary>
    	/// This Operation's operation description object.
    	/// </summary>
    	private OperationDescription opDesc;
    	/// <summary>
    	/// The list of roles that are allowed to execute this operation
    	/// </summary>
    	private string[] rolesAllowed = null;
    	
    	#region IOperationBehavior Members
    	
    	public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) { }
    	public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { }
    	public void Validate(OperationDescription operationDescription) { }
    	
    	public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) {
    		opDesc = operationDescription;
    		
    		object[] customAttributes = opDesc.SyncMethod.GetCustomAttributes(true);
    		
    		// We need to have some roles defined that are allowed to access this method
    		MyRolesAllowedAttribute rolesAtt = null;
    		
    		foreach (object customAtt in customAttributes) {
    			if (customAtt.GetType() == typeof(MyRolesAllowedAttribute)) {
    				rolesAtt = (MyRolesAllowedAttribute)customAtt;
    			}
    		}
    		
    		if (rolesAtt != null) {
    			this.rolesAllowed = rolesAtt.RolesAllowed;
    		}
    		
    		dispatchOperation.ParameterInspectors.Add(this);
    	}
    	
    	#endregion
    	
    	#region IParameterInspector Members
    	
    	public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState) { }
    	
    	public object BeforeCall(string operationName, object[] inputs) {
    		// We need to have some roles defined that are allowed to access this operation.
    		if (this.rolesAllowed == null ) {
    			throw new WebProtocolException(System.Net.HttpStatusCode.BadRequest, "No roles have been specified as being allowed to access this operation, and so access is denied.", new SecurityException());
    		}
    		
    		// (check HTTP user/pass authorization here and make sure the user is in one of the
    		// operation's rolesAllowed; otherwise throw an exception...)
    		
    		// No exceptions thrown; access allowed.  Returning null indicates that we don't want/need to
    		// use correlation state...
    		return null;
    	}
    	
    	#endregion
    }

    With the authorization code checking for the method's MyRolesAllowedAttribute, it's left to us to apply that to all methods we create for the service (and if we forget to do that, access to the operation is denied for all, which is good).  The code we use for that, in Service.svc.cs, looks something like this:

    public partial class Service {
    
    	// [...]
    
    	[WebHelp(Comment = "Gets a user's details.")]
    	[WebGet(UriTemplate = "users/{username}")]
    	[MyRolesAllowed(RolesAllowed = new string[] { "AccountHolder", "Member", "AdminUser" })]
    	[OperationContract]
    	public User GetUser(string Username) {
    		return new User(Username);
    	}
    	
    	// [...]
    }
    
    // [...]
    
    /// <summary>
    /// A list of roles that are allowed to execute this operation.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class MyRolesAllowedAttribute: Attribute {
    	public MyRolesAllowedAttribute() { }
    	
    	/// <summary>
    	/// The list of roles.
    	/// </summary>
    	public string[] RolesAllowed { get; set; }
    }

    So, that achieves the functionality I'm looking for.

    The only thing is, it seems slightly clunky because I'm having to add a MyOperationAuthorizer instance for EVERY single operation in the service.  I'm not sure how .NET deals with this, but it could perhaps get quite a resource hog if lots of operations were implemented for the service.  As this is code that we always want to apply to every operation in the service, would it be more appropriate to somehow do this authorization in a class implementing the IServiceBehavior Interface, or am I doing it the 'right' way now?

    === Jez ===
    • Marked as answer by Jez9999 Friday, August 21, 2009 10:13 AM
    • Edited by Jez9999 Friday, August 21, 2009 10:22 AM
    Friday, August 21, 2009 10:13 AM

All replies

  • You can use an IOperationBehavior + IParameterInspector to accomplish what you need. The example below shows a very basic authorization mechanism per method in a REST service.

        public class Post_7cdb680f_8d71_4f11_b5b8_3deafb621fba
        {
            [ServiceContract(Namespace = "")]
            public interface ITest
            {
                [OperationContract]
                [WebInvoke(BodyStyle = WebMessageBodyStyle.Wrapped)]
                string EchoString(string text);
                [OperationContract]
                [MyParamInspector("admin")]
                [WebInvoke(BodyStyle = WebMessageBodyStyle.Wrapped)]
                int Add(int x, int y);
            }
            public class Service : ITest
            {
                public string EchoString(string text)
                {
                    return text;
                }
                public int Add(int x, int y)
                {
                    return x + y;
                }
            }
            public class MyParamInspectorAttribute : Attribute, IOperationBehavior, IParameterInspector
            {
                private string roles;
                public MyParamInspectorAttribute(string roles)
                {
                    this.roles = roles;
                }
                #region IOperationBehavior Members
                public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
                {
                }
                public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
                {
                }
                public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
                {
                    dispatchOperation.ParameterInspectors.Add(this);
                }
                public void Validate(OperationDescription operationDescription)
                {
                }
                #endregion
    
                #region IParameterInspector Members
                public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
                {
                }
                public object BeforeCall(string operationName, object[] inputs)
                {
                    string pwd = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters["password"];
                    string user = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters["user"];
                    if (user != pwd)
                    {
                        throw new SecurityException("Unauthorized");
                    }
                    if (user != this.roles)
                    {
                        throw new SecurityException("Unauthorized");
                    }
                    return null;
                }
                #endregion
            }
            static Binding GetBinding()
            {
                WebHttpBinding result = new WebHttpBinding();
                return result;
            }
            public static void Test()
            {
                string baseAddress = "http://localhost:8000/Service";
                ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
                host.AddServiceEndpoint(typeof(ITest), GetBinding(), "").Behaviors.Add(new WebHttpBehavior());
                host.Open();
                Console.WriteLine("Host opened");
    
                string echoStringRequest = "<EchoString><text>Hello</text></EchoString>";
                string addRequest = "<Add><x>3</x><y>4</y></Add>";
    
                Console.WriteLine("Echo string without credentials");
                Util.SendRequest(baseAddress + "/EchoString", "POST", "text/xml", echoStringRequest);
    
                Console.WriteLine("Add without credentials");
                Util.SendRequest(baseAddress + "/Add", "POST", "text/xml", addRequest);
    
                Console.WriteLine("Add with credentials");
                Util.SendRequest(baseAddress + "/Add?user=admin&password=admin", "POST", "text/xml", addRequest);
    
                Console.Write("Press ENTER to close the host");
                Console.ReadLine();
                host.Close();
            }
        }
        public static class Util
        {
            public static void SendRequest(string uri, string method, string contentType, string body)
            {
                HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri);
                req.Method = method;
                if (!String.IsNullOrEmpty(contentType)) req.ContentType = contentType;
                if (!String.IsNullOrEmpty(body))
                {
                    byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
                    req.GetRequestStream().Write(bodyBytes, 0, bodyBytes.Length);
                    req.GetRequestStream().Close();
                }
                else if (method == "POST")
                {
                    req.ContentLength = 0;
                }
                HttpWebResponse resp;
                try
                {
                    resp = (HttpWebResponse)req.GetResponse();
                }
                catch (WebException e)
                {
                    resp = (HttpWebResponse)e.Response;
                }
                Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
                if (resp.ContentLength > 0)
                {
                    Console.WriteLine(new StreamReader(resp.GetResponseStream()).ReadToEnd());
                }
                else
                {
                    Console.WriteLine("Received a response of 0 bytes");
                }
            }
        }
    
    • Marked as answer by Bin-ze Zhao Tuesday, August 18, 2009 8:44 AM
    • Unmarked as answer by Jez9999 Friday, August 21, 2009 10:13 AM
    Friday, August 14, 2009 4:13 PM
  • Carlos,

    Not quite what I needed, but thanks, because your code led me to the solution I did need, eventually.  :-)

    The thing is, your code means that, to apply the security check to a given operation, you need to remember to apply the 'MyParamInspector' attribute to that operation.  So, by default, the operation is allowed as that attribute isn't applied and the security code doesn't run.  I wanted to 'punish' the developer for forgetting to apply such an attribute, by defaulting to 'no access allowed'.  What I did end up doing, though, was applying my IOperationBehavior/IParameterInspector class to EVERY operation, automatically, programatically.  This is done in my web service host, which extends WebServiceHost2, and the IOperationBehavior/IParameterInspector class is applied in the OnOpening() method, like so:

    // Attach authorization code to each and every operation
    foreach (var ep in this.Description.Endpoints) {
    	foreach (var op in ep.Contract.Operations) {
    		op.Behaviors.Add(new MyOperationAuthorizer());
    	}
    }

    The actual authorizer itself then checks to see whether the method has had my necessary custom roles attribute applied to it, and if not, automatically denies access to the operation.  If it has, it can then check to see whether the user is in one of the necessary roles:

    public class MyOperationAuthorizer : IOperationBehavior, IParameterInspector {
    	/// <summary>
    	/// This Operation's operation description object.
    	/// </summary>
    	private OperationDescription opDesc;
    	/// <summary>
    	/// The list of roles that are allowed to execute this operation
    	/// </summary>
    	private string[] rolesAllowed = null;
    	
    	#region IOperationBehavior Members
    	
    	public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) { }
    	public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { }
    	public void Validate(OperationDescription operationDescription) { }
    	
    	public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) {
    		opDesc = operationDescription;
    		
    		object[] customAttributes = opDesc.SyncMethod.GetCustomAttributes(true);
    		
    		// We need to have some roles defined that are allowed to access this method
    		MyRolesAllowedAttribute rolesAtt = null;
    		
    		foreach (object customAtt in customAttributes) {
    			if (customAtt.GetType() == typeof(MyRolesAllowedAttribute)) {
    				rolesAtt = (MyRolesAllowedAttribute)customAtt;
    			}
    		}
    		
    		if (rolesAtt != null) {
    			this.rolesAllowed = rolesAtt.RolesAllowed;
    		}
    		
    		dispatchOperation.ParameterInspectors.Add(this);
    	}
    	
    	#endregion
    	
    	#region IParameterInspector Members
    	
    	public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState) { }
    	
    	public object BeforeCall(string operationName, object[] inputs) {
    		// We need to have some roles defined that are allowed to access this operation.
    		if (this.rolesAllowed == null ) {
    			throw new WebProtocolException(System.Net.HttpStatusCode.BadRequest, "No roles have been specified as being allowed to access this operation, and so access is denied.", new SecurityException());
    		}
    		
    		// (check HTTP user/pass authorization here and make sure the user is in one of the
    		// operation's rolesAllowed; otherwise throw an exception...)
    		
    		// No exceptions thrown; access allowed.  Returning null indicates that we don't want/need to
    		// use correlation state...
    		return null;
    	}
    	
    	#endregion
    }

    With the authorization code checking for the method's MyRolesAllowedAttribute, it's left to us to apply that to all methods we create for the service (and if we forget to do that, access to the operation is denied for all, which is good).  The code we use for that, in Service.svc.cs, looks something like this:

    public partial class Service {
    
    	// [...]
    
    	[WebHelp(Comment = "Gets a user's details.")]
    	[WebGet(UriTemplate = "users/{username}")]
    	[MyRolesAllowed(RolesAllowed = new string[] { "AccountHolder", "Member", "AdminUser" })]
    	[OperationContract]
    	public User GetUser(string Username) {
    		return new User(Username);
    	}
    	
    	// [...]
    }
    
    // [...]
    
    /// <summary>
    /// A list of roles that are allowed to execute this operation.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class MyRolesAllowedAttribute: Attribute {
    	public MyRolesAllowedAttribute() { }
    	
    	/// <summary>
    	/// The list of roles.
    	/// </summary>
    	public string[] RolesAllowed { get; set; }
    }

    So, that achieves the functionality I'm looking for.

    The only thing is, it seems slightly clunky because I'm having to add a MyOperationAuthorizer instance for EVERY single operation in the service.  I'm not sure how .NET deals with this, but it could perhaps get quite a resource hog if lots of operations were implemented for the service.  As this is code that we always want to apply to every operation in the service, would it be more appropriate to somehow do this authorization in a class implementing the IServiceBehavior Interface, or am I doing it the 'right' way now?

    === Jez ===
    • Marked as answer by Jez9999 Friday, August 21, 2009 10:13 AM
    • Edited by Jez9999 Friday, August 21, 2009 10:22 AM
    Friday, August 21, 2009 10:13 AM