Authentication Token Service for WCF Services (Part 3 – Token Validation in IDispatchMessageInspector)
In Authentication Token Service for WCF Services (Part 2 – Database Authentication) we showed how to verify our token. However, we verified the token in the service itself.
This is not ideal.
[OperationContract] [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Bare)] public string Test() { var token = HttpContext.Current.Request.Headers["Token"]; using (var dbContext = new BasicTokenDbContext()) { ITokenValidator validator = new DatabaseTokenValidator(dbContext); if (validator.IsValid(token)) { // Do service work here . . . } } }
This is fine for a one or two services. But what if there are going to have many services? The Don’t Repeat Yourself (DRY) principle would be broken if we repeated the same lines of code at the top of every service. If only we could validate the token in one place, right? Well, we can.
We could make a method that we could call at the top of every service, but even if we did that, we would still have to repeat one line for every service. Is there a way where we wouldn’t even have to repeat a single line of code? Yes, there is. Using Aspect-oriented programming (AOP). It turns out WCF services have some AOP capabilities built in.
IDispatchMessageInspector can be configured to do this.
To enable this, your really need to implement three Interfaces and configure it in the web.config. I am going to use separate classes for each interface.
The web config extension class:
using System; using System.ServiceModel.Configuration; namespace WcfSimpleTokenExample.Behaviors { public class TokenValidationBehaviorExtension : BehaviorExtensionElement { #region BehaviorExtensionElement public override Type BehaviorType { get { return typeof(TokenValidationServiceBehavior); } } protected override object CreateBehavior() { return new TokenValidationServiceBehavior(); } #endregion } }
The Service Behavior class:
using System.Collections.ObjectModel; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; namespace WcfSimpleTokenExample.Behaviors { public class TokenValidationServiceBehavior : IServiceBehavior { public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { foreach (var t in serviceHostBase.ChannelDispatchers) { var channelDispatcher = t as ChannelDispatcher; if (channelDispatcher != null) { foreach (var endpointDispatcher in channelDispatcher.Endpoints) { endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new TokenValidationInspector()); } } } } public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } } }
The message inspector class
using System.Net; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Dispatcher; using System.ServiceModel.Web; using WcfSimpleTokenExample.Business; using WcfSimpleTokenExample.Database; using WcfSimpleTokenExample.Interfaces; namespace WcfSimpleTokenExample.Behaviors { public class TokenValidationInspector : IDispatchMessageInspector { public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) { // Return BadRequest if request is null if (WebOperationContext.Current == null) { throw new WebFaultException(HttpStatusCode.BadRequest); } // Get Token from header var token = WebOperationContext.Current.IncomingRequest.Headers["Token"]; // Validate the Token using (var dbContext = new BasicTokenDbContext()) { ITokenValidator validator = new DatabaseTokenValidator(dbContext); if (!validator.IsValid(token)) { throw new WebFaultException(HttpStatusCode.Forbidden); } // Add User ids to the header so the service has them if needed WebOperationContext.Current.IncomingRequest.Headers.Add("User", validator.Token.User.Username); WebOperationContext.Current.IncomingRequest.Headers.Add("UserId", validator.Token.User.Id.ToString()); } return null; } public void BeforeSendReply(ref Message reply, object correlationState) { } } }
Basically, what happens is AfterReceiveRequest is called somewhere between when the actual packets arrive at the server and just before the service is called. This is perfect. We can validate our token here in a single place.
So let’s populate our AfterReceiveRequest.
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) { // Return BadRequest if request is null if (WebOperationContext.Current == null) { throw new WebFaultException(HttpStatusCode.BadRequest); } // Get Token from header var token = WebOperationContext.Current.IncomingRequest.Headers["Token"]; // Validate the Token using (var dbContext = new BasicTokenDbContext()) { ITokenValidator validator = new DatabaseTokenValidator(dbContext); if (!validator.IsValid(token)) { throw new WebFaultException(HttpStatusCode.Forbidden); } // Add User ids to the header so the service has them if needed WebOperationContext.Current.IncomingRequest.Headers.Add("User", validator.Token.User.Username); WebOperationContext.Current.IncomingRequest.Headers.Add("UserId", validator.Token.User.Id.ToString()); } return null; }
You might have noticed we made one change to the ITokenValidator. See the changes below. It now has a Token property, as does its implementation, DatabaseTokenValidator. Mostly I am getting Token.UserId, but since EF gets the User object for me too, I went ahead an added the User name as well.
using WcfSimpleTokenExample.Database; namespace WcfSimpleTokenExample.Interfaces { public interface ITokenValidator { bool IsValid(string token); Token Token { get; set; } } }
using System; using System.Linq; using WcfSimpleTokenExample.Database; using WcfSimpleTokenExample.Interfaces; namespace WcfSimpleTokenExample.Business { public class DatabaseTokenValidator : ITokenValidator { // Todo: Set this from a web.config appSettting value public static double DefaultSecondsUntilTokenExpires = 1800; private readonly BasicTokenDbContext _DbContext; public DatabaseTokenValidator(BasicTokenDbContext dbContext) { _DbContext = dbContext; } public bool IsValid(string tokentext) { Token = _DbContext.Tokens.SingleOrDefault(t => t.Text == tokentext); return Token != null && !IsExpired(Token); } internal bool IsExpired(Token token) { var span = DateTime.Now - token.CreateDate; return span.TotalSeconds > DefaultSecondsUntilTokenExpires; } public Token Token { get; set; } } }
Now we don’t need all that Token validation code in our Service. We can clean it up. In fact, since all it does right now is return a string, our service only needs a single line of code. I also added the UserId and User to the output for fun.
[ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class Test1Service { [OperationContract] [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Bare)] public string Test() { return string.Format("Your token worked! User: {0} User Id: {1}", WebOperationContext.Current.IncomingRequest.Headers["UserId"], WebOperationContext.Current.IncomingRequest.Headers["User"]); } }
Well, now that it is all coded up, it won’t work until we enable the new behavior in the web.config. So let’s look at the new web.config. We create a new ServiceBehavior (lines 34-38) for all the services that validate the token. We leave the AuthenticationTokenService the same as we don’t have a token when we hit it because we hit it to get the token. We also need to make sure to add the behavior extension (lines 41-46). Then we need to tell our ServiceBehavior to use the new extension (line 37).
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <appSettings> <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" /> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> </system.web> <system.serviceModel> <services> <service name="WcfSimpleTokenExample.Services.AuthenticationTokenService" behaviorConfiguration="ServiceBehaviorHttp"> <endpoint address="" behaviorConfiguration="AjaxEnabledBehavior" binding="webHttpBinding" contract="WcfSimpleTokenExample.Services.AuthenticationTokenService" /> </service> <service name="WcfSimpleTokenExample.Services.Test1Service" behaviorConfiguration="ServiceAuthBehaviorHttp"> <endpoint address="" behaviorConfiguration="AjaxEnabledBehavior" binding="webHttpBinding" contract="WcfSimpleTokenExample.Services.Test1Service" /> </service> </services> <behaviors> <endpointBehaviors> <behavior name="AjaxEnabledBehavior"> <webHttp helpEnabled="true" /> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior name="ServiceBehaviorHttp"> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="true" /> </behavior> <behavior name="ServiceAuthBehaviorHttp"> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="true" /> <TokenValidationBehaviorExtension /> </behavior> </serviceBehaviors> </behaviors> <extensions> <behaviorExtensions> <add name="TokenValidationBehaviorExtension" type="WcfSimpleTokenExample.Behaviors.TokenValidationBehaviorExtension, WcfSimpleTokenExample, Version=1.0.0.0, Culture=neutral"/> </behaviorExtensions> </extensions> <serviceHostingEnvironment aspNetCompatibilityEnabled="false" multipleSiteBindingsEnabled="true" /> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true" /> <directoryBrowse enabled="true" /> </system.webServer> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v11.0" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> <connectionStrings> <add name="BasicTokenDbConnection" connectionString="data source=(LocalDB)\v11.0;attachdbfilename=|DataDirectory|\BasicTokenDatabase.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>
Go on and read part 4 here: Authentication Token Service for WCF Services (Part 4 – Supporting Basic Authentication)
It is all on GitHub now. https://www.rhyous.com/2015/04/14/basic-token-service-for-wcf-services-part-3-token-validation-in-idispatchmessageinspector/
link on dropbox not working, please may you give it to me, i am trying to teach wcf auth
It is all on GitHub now. https://www.rhyous.com/2015/04/14/basic-token-service-for-wcf-services-part-3-token-validation-in-idispatchmessageinspector/
when i add TokenValidationBehaviorExtension to web config ,i have error why?
Hi,Rhyous
Great articule. I haven't get hands into it but I'll do right away. Could you have a client simple on PHP?
Best regards.
Hi,
I liked your article but I did not solve one thing I wanna add user roles as well what is the best practice for it regarding your AOP solution.
Cheers
This article is about Authentication. You are talking about Authorization. While related, authorization is a completely separate topic that occurs using the authenticated user but after authentication.
There are articles aplenty about Authorization.
Hi,
I liked your article very much.
will you be kind enough to guide a Client which first takes Token & then pass on to every request, like you did for Server code, we need client code as well where any windows based app or web based app first login & take token & then that token will be used in Every request so that user should follow DRY principle.
I am currently using a JavaScript client. First, I authenticate, and store the token as a cookie. Then for subsequent ajax calls, I created a beforeSend function that is a member of a global object.
The beforeSend method works with the Ajax object's beforeSend property. Whenever I make an ajax call, I pass in siteInfo.beforeSend.
http://www.w3schools.com/jquery/ajax_ajax.asp
I haven't written a C# client yet. I would likely re-enable SOAP and run svcutil.exe against my site to get my client files.
Obviously I need a "
Then as my example uses HTTP headers, not SOAP headers, I would use something like this:
http://stackoverflow.com/questions/13856362/adding-http-request-header-to-wcf-request
Obviously I need another article: Basic Token Service for WCF Services - Client Side.
Hi first of all thanks for this great work.
I had observed that your given code does not work & it failed to find service saying.
authorization service works & displays metadata whereas second service gives below error
The server was unable to process the request due to an internal error. For more information about the error, either turn on IncludeExceptionDetailInFaults (either from ServiceBehaviorAttribute or from the configuration behavior) on the server in order to send the exception information back to the client, or turn on tracing as per the Microsoft .NET Framework SDK documentation and inspect the server trace logs.
please try to arrange basic client as well (Jquery or C# both will do) in this project so we get full idea since when I discover service only authentication service works & not other service
TokenValidationBehaviorExtension
finally using Firefox rest client I am able to check part 3 code working properly.
I was creating new project to get service reference but only AuthenticationTokenService was being added & when I tried for Test1Service it always failed for internal error.
now waiting for C# client code also trying my best to make one.
I am unable to create the client because while adding Service Reference for test1service i get error
http://localhost:49911/Services/Test1Service.svc/_vti_bin/ListData.svc/$metadata'.
The request failed with HTTP status 400: Bad Request.
Metadata contains a reference that cannot be resolved:
I have a javascript client example now.