.netrestwcf

Pass API key / auth information from HttpOperationHandler to API class in WCF4 webservice


I'm currently developing an WCF4 based REST webservice. Most calls don't need authorization, but some do, so my plan is to implement an RequireAuthorization attribute that handles API key authorization for some calls.

After reading Implementing an Authorization Attribute for WCF Web API I implemented an HttpOperationHandler that handles the API key validation in the OnHandle method and added it to the RequestHandlers in the service configuration.

Basically this works fine, but I need to pass the user info I extracted in the AuthOperationHandler to the service, but I can't find any information on this.

Can anyone help?

public class ServiceConfiguration : WebApiConfiguration
{
    public ServiceConfiguration()
    {
        RequestHandlers = (c, e, od) => {
            var authorizeAttribute = od.Attributes.OfType<RequireAuthorizationAttribute>().FirstOrDefault();
            if (authorizeAttribute != null)
            {
                c.Add(new AuthOperationHandler(authorizeAttribute));
            }
        };

        CreateInstance = (serviceType, context, request) => RepositoryFactory.GetInstance(serviceType);
    }
}

public class AuthOperationHandler : HttpOperationHandler<HttpRequestMessage, HttpRequestMessage>
{
    protected override HttpRequestMessage OnHandle(HttpRequestMessage input)
    {
        // API Key validation here
        var user = RetrieveUserByApiKey(input);

        return input;
    }
}

Solution

  • I found a solution to this, but it does come with a caveat.

    The way I did it was for my operation handler to return some form of custom authentication state class instead of a HttpResponsemessage - in my case I used an AuthenticationTicket.

    public class AuthOperationHandler : HttpOperationHandler<HttpRequestMessage, AuthenticationTicket>
    {
        protected override AuthenticationTicket OnHandle(HttpRequestMessage input)
        {
            var ticket = RetrieveAuthenticationTicket(input);
    
            return ticket;
        }
    }
    

    And the AuthenticationTicket class just stores the account ID.

        public class AuthenticationTicket
        {
            public AuthenticationTicket(int accountId)
            {
                AccountId = accountId;
            }
    
            public int AccountId { get; set; }
        }
    

    Then in your service method you add an AuthenticationTicket as a parameter and this will be populated for you by the operation handler.

        [OperationContract]
        [WebInvoke(UriTemplate = "people", Method = "GET")]
        HttpResponseMessage<IList<Person>> LoadPeople(AuthenticationTicket ticket);
    

    You can then use the ticket in your method implementation.

    However, this acts a bit funny with POST, PUT and DELETE operations if you need to use an additional parameter, like so:

        [OperationContract]
        [WebInvoke(UriTemplate = "people", Method = "PUT")]
        HttpResponseMessage AddPerson(AuthenticationTicket ticket, Person person);
    

    You'll get an exception along the following lines:

    The HttpOperationHandlerFactory is unable to determine the input parameter that should be associated with the request message content for service operation 'AddPerson'. If the operation does not expect content in the request message use the HTTP GET method with the operation. Otherwise, ensure that one input parameter either has it's IsContentParameter property set to 'True' or is a type that is assignable to one of the following: HttpContent, ObjectContent1, HttpRequestMessage or HttpRequestMessage1.

    The solution I found (not sure whether it's right) is to wrap the resource in a HttpRequestMessage like so:

        [OperationContract]
        [WebInvoke(UriTemplate = "people", Method = "PUT")]
        HttpResponseMessage UpdatePerson(AuthenticationTicket ticket, HttpRequestMessage<Person> request);
    

    Then in your method use the following to retrieve the Person:

    var person = request.Content.ReadAsAsync<Person>().Result;
    

    Hope this helps, it works for me. But I'm still learning myself to be honest, so there may be better ways of doing it.