Authentication Token Service for WCF Services (Part 4 – Supporting Basic Authentication)
In Authentication Token Service for WCF Services (Part 3 – Token Validation in IDispatchMessageInspector) we showed how to verify our token against a database. The token is a great tool. The authentication service also provides the token based on a post of credentials.
In this article, we are going to add support for Basic Authentication. We aren’t going to do it the standard WCF way, using Transport security. We will keep our security at none, expect the deployment to be https and roll our own code to handle Basic Authentication.
Download this project here: WCF Basic Auth
There are two features we want in order claim support Basic Authentication.
- Allow AuthenticationTokenService.svc to create the token by optionally using Basic Authentication.
- Allow Basic Authentication as an option to providing a token.
To provide these two features, first we have to understand Basic Authentication. Basic Authentication is a well-known standard that is defined.
Basic Authentication is an html request header. The header is named “Authorization” and the value is as follows:
Basic amFyZWQ6dGVzdHB3
The first part of the Authorization header value is just the word “Basic” followed by a space.
The second part is the username and password concatenated together with a semicolon separator and then Base64 encoded.
jared:testpw
Basic amFyZWQ6dGVzdHB3
Let’s start with a simple class to manage the Basic authentication header, and encoding and decoding it.
using System; using System.Text; using WcfSimpleTokenExample.Model; namespace WcfSimpleTokenExample.Business { public class BasicAuth { private readonly string _User; private readonly string _Password; private const string Prefix = "Basic "; #region Constructors public BasicAuth(string encodedHeader) : this(encodedHeader, Encoding.UTF8) { } public BasicAuth(string encodedHeader, Encoding encoding) { HeaderValue = encodedHeader; var decodedHeader = encodedHeader.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) ? encoding.GetString(Convert.FromBase64String(encodedHeader.Substring(Prefix.Length))) : encoding.GetString(Convert.FromBase64String(encodedHeader)); var credArray = decodedHeader.Split(':'); if (credArray.Length > 0) _User = credArray[0]; if (credArray.Length > 1) _Password = credArray[1]; } public BasicAuth(string user, string password) : this(user, password, Encoding.UTF8) { } public BasicAuth(string user, string password, Encoding encoding) { _User = user; _Password = password; HeaderValue = Prefix + Convert.ToBase64String(encoding.GetBytes(string.Format("{0}:{1}", _User, _Password))); } #endregion public Credentials Creds { get { return _Creds ?? (_Creds = new Credentials { User = _User, Password = _Password }); } } private Credentials _Creds; public string HeaderValue { get; } } }
BasicAuth.cs has constructors that allow for encoding by passing in a username and password and encoding it, as well as constructors that allow for passing in the header value and decoding it to get the username and password.
If we add BasicAuth.cs to our existing WcfSimpleTokenExample project, we can easily use it to support Basic Authentication.
Feature 1 – Basic Authentication for AuthenticationTokenService.svc/Authenticate
By using the BasicAuth.cs class, we can provide support for Basic Authentication in our token service using only 3 lines of code. Below is our new AuthenticationTokenService.svc.cs. Lines 18-20 our the new lines we add.
using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using WcfSimpleTokenExample.Business; using WcfSimpleTokenExample.Database; using WcfSimpleTokenExample.Model; namespace WcfSimpleTokenExample.Services { [ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class AuthenticationTokenService { [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Bare)] [OperationContract] public string Authenticate(Credentials creds) { if (creds == null && WebOperationContext.Current != null) { creds = new BasicAuth(WebOperationContext.Current.IncomingRequest.Headers["Authorization"]).Creds; } using (var dbContext = new BasicTokenDbContext()) { return new DatabaseTokenBuilder(dbContext).Build(creds); } } } }
Feature 2 – Using Basic Authentication instead of a token
In our TokenValidationInspector.cs file, we are already validating the token using DatabaseTokenValidator, Now we need to validate the crendentials. We can validate credentials using the DatabaseCrendentialsValidator object that is already being used by AuthenticationTokenBuilder. However, we have to add some conditionaly code to test if a token is provided or if Basic Authorization is provided. If both are ignored, the token takes priority.
To do this, I wrapped the existing lines calling DatabaseTokenValidator into a method called ValidateToken. THen I created a new method called ValidateBasicAuthentication, which we only attempt to call a token isn’t provided.
using System.Net; using System.Security.Authentication; 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"]; if (!string.IsNullOrWhiteSpace(token)) { ValidateToken(token); } else { ValidateBasicAuthentication(); } return null; } private static void ValidateToken(string 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()); } } private static void ValidateBasicAuthentication() { var authorization = WebOperationContext.Current.IncomingRequest.Headers["Authorization"]; if (string.IsNullOrWhiteSpace(authorization)) { using (var dbContext = new BasicTokenDbContext()) { var basicAuth = new BasicAuth(authorization); if (!new DatabaseCredentialsValidator(dbContext).IsValid(basicAuth.Creds)) { throw new AuthenticationException(); } } } } public void BeforeSendReply(ref Message reply, object correlationState) { } } }
The web.config
There are not changes needed for the web.config. Here is a copy of it though, for reference.
<?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="ServiceRequiresTokenBehaviorHttp"> <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="ServiceRequiresTokenBehaviorHttp"> <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>
Testing Basic Authentication with PostMan
Now we an test that this is working using PostMan. Our PostMan call is similar to what we did in previous articles, but instead of passing a token header, we set Basic Authentication, which sets the Authorization header for us (yes, you could have set the Authorization header manually.)
You could create the Authorization header yourself, but PostMan will create it for you if you click the Authorization and select Basic Auth. Enter your username and password and click update.
All this does it create an Authorization header for you. You can see this by clicking on the Headers tab in PostMan.
Go ahead and click Send and you will get your authentication.
Notice the url is https in the image. I haven’t shown you how to do that yet. That is in part 5 here: Authentication Token Service for WCF Services (Part 5 – Adding SSL)