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:
- Deny access when the Access Token is missing the 'Read' scope.
- 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.