Restricting WCF services by scopes

Restricting WCF services by scopes

24 January 2015

I wanted my WCF service to Authorise requests that supply an AccessToken with a Scope of 'Read'.

The ServiceContract with bespoke AllowedScopes attribute

Below you will see the ServiceContract for my HelloWorldService.

[ServiceContract]
public interface IHelloWorldService
{
    [WebGet(UriTemplate = "/hello", BodyStyle = WebMessageBodyStyle.Bare)]
    [OperationContract]
    [AllowedScopes("read")]
    Stream GetHello();
}

AllowedScopes is an Attribute I created to decorate the method 'GetHello'.
This allows me to later infer the AllowedScopes attribute in a DispatchMethodInspector during runtime.

This means any requests to /hello must supply an Access Token with the Read scope.

The tests

I found it impossible to test my DispatchMethodInspector in isolation due to WCF being composed of sealed classes.
Instead, I had to host a service that made use of the DispatchMethodInspector.

So, I wrote two tests:

  1. Deny access when the Access Token is missing the 'Read' scope.
  2. Allow access when the Access Token supplies the 'Read' scope.
/// <summary>
/// Should be denied if a bearer token is missing 'read' scope.
/// <para/>
/// GET /hello
///  + Request 
///     + Headers 
///         + Authorization Bearer {JWT}
/// 
/// + Response (401)
/// </summary>
[Fact]
public void ShouldDenyWhenWhenBearerTokenMissingReadScope()
{
    var securityToken = new JwtSecurityToken("http://chwilliamson.me.uk",
        "http://chwilliamson.me.uk/audience", new[] {new Claim("scope", "invalid")},
        new Lifetime(DateTime.UtcNow, DateTime.UtcNow.AddDays(1)),
        new SigningCredentials(new InMemorySymmetricSecurityKey(Encoding.ASCII.GetBytes("Colin is great and has created this key")),
            "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", "http://www.w3.org/2001/04/xmlenc#sha256"));

    var token = new JwtSecurityTokenHandler().WriteToken(securityToken);

    using (Host.OpenSimpleHelloWorldServiceWithAuthorization(baseUri))
    {
        var resourceUri = new Uri(baseUri, "hello");
        var request = WebRequest.Create(resourceUri);
        request.Headers.Add("Authorization","Bearer " + token);

        var webException = Assert.Throws<WebException>(() =>
        {
            request.GetResponse();
        });

        var httpRequest = webException.Response as HttpWebResponse;
        Assert.NotNull(httpRequest);
        Assert.Equal(HttpStatusCode.Unauthorized, httpRequest.StatusCode);
    }
}

/// <summary>
/// Should be successful if bearer token has read scope.
/// <para/>
/// GET /hello
/// 
/// + Request 
///     + Headers 
///         + Authorization Bearer {JWT}
/// 
/// + Response (401)
/// </summary>
[Fact]
public void ShouldSucceedWhenBearerTokenHasReadScope()
{
    var securityToken = new JwtSecurityToken("http://chwilliamson.me.uk",
        "http://chwilliamson.me.uk/audience", new[] { new Claim("scope", "read") },
        new Lifetime(DateTime.UtcNow, DateTime.UtcNow.AddDays(1)),
        new SigningCredentials(new InMemorySymmetricSecurityKey(Encoding.ASCII.GetBytes("Colin is great and has created this key")),
            "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", "http://www.w3.org/2001/04/xmlenc#sha256"));

    var token = new JwtSecurityTokenHandler().WriteToken(securityToken);

    using (Host.OpenSimpleHelloWorldServiceWithAuthorization(baseUri))
    {
        var resourceUri = new Uri(baseUri, "hello");
        var request = WebRequest.Create(resourceUri);
        request.Headers.Add("Authorization", "Bearer " + token);

        var response =  request.GetResponse() as HttpWebResponse;
        Assert.NotNull(response);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        using (var sr = new StreamReader(response.GetResponseStream()))
        {
            Assert.Equal("Hello World", sr.ReadToEnd());
        }
    }
}

The DispatchMessageInspector

Below is my implementation of IDispatchMessageInspector called AuthorizationDispatchMessageInspector.
It will infer the scopes from the Contract using some reflection to query the AllowedScopes attribute during runtime.

/// <summary>
/// Will inspect the HttpRequest Authorization header.
/// </summary>
public class AuthorizationDispatchMessageInspector : IDispatchMessageInspector
{
    private static readonly Action<HttpResponseMessageProperty> UnathorisedAction = property => property.StatusCode = HttpStatusCode.Unauthorized;
    /// <summary>
    /// Check the authorization header.
    /// </summary>
    /// <param name="request"></param>
    /// <param name="channel"></param>
    /// <param name="instanceContext"></param>
    /// <returns></returns>
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        var requestMessage = request.Properties["httpRequest"] as HttpRequestMessageProperty;
        if (requestMessage == null) return UnathorisedAction;
        var bearer = requestMessage.Headers["Authorization"];
        if (bearer == null) return UnathorisedAction;
        var token = bearer.Split(' ')[1];


        var accessToken = new JwtSecurityToken(token);
        var parameters = new TokenValidationParameters
        {
            ValidIssuer = "http://chwilliamson.me.uk",
            AllowedAudience = "http://chwilliamson.me.uk/audience",
            SigningToken = new BinarySecretSecurityToken(Encoding.ASCII.GetBytes("Colin is great and has created this key"))
        };

        try
        {
            var tokenClaimsPrincipal = new JwtSecurityTokenHandler().ValidateToken(accessToken, parameters);
            ClaimsPrincipal.Current.AddIdentities(tokenClaimsPrincipal.Identities);

            var serviceType = instanceContext.Host.Description.Endpoints.First().Contract.ContractType;
            var operationName = OperationContext.Current.IncomingMessageProperties["HttpOperationName"] as string;
            if (operationName == null) return UnathorisedAction;
            var allowedScopes = new List<string>();
            //any attributes
            var matchingMethod = serviceType.GetMethod(operationName);
            var scopesAttribute = matchingMethod.GetCustomAttributes<AllowedScopesAttribute>().FirstOrDefault();
            if (scopesAttribute != null)
            {
                allowedScopes.AddRange(scopesAttribute.Scopes);
            }

            if (allowedScopes.Any(allowedScope => !tokenClaimsPrincipal.Claims.Any(scope => scope.Type == "scope" && scope.Value == allowedScope)))
            {
                return UnathorisedAction;
            }
            return null;
        }
        catch
        {
            return UnathorisedAction;
        }
    }

    /// <summary>
    /// Concoct a response.
    /// </summary>
    /// <param name="reply"></param>
    /// <param name="correlationState"></param>
    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        var action = (Action<HttpResponseMessageProperty>) correlationState;
        if (action == null) return;
        var response = (HttpResponseMessageProperty) reply.Properties["httpResponse"];
        action(response);
    }
}

Download my WCF Research sample

Feel free to download my WCF Research Source Code.

.NET SelfHost Testing WCF XUnit