Replacing the built-in Basic Authentication Module to support non-English characters in a HttpWebRequest

 

A few weeks ago I was trying to deploy a site using Web Deploy and authenticate using a non-English user name. The operation would succeed on some computers but will fail on others. Since I was using the Web Deploy API, I debugged into the .NET framework code and found that the framework encodes the user name using the default code page. That made it work for some computers but not others, depending on what the default code page was on the local computer and the remote server.

Sounds confusing? Let me start from the bottom up.

Basic Authentication

When Web Deploy connects to the remote server it can use basic authentication to authenticate with the server over HTTP/S. That’s the method we chose to authenticate with and since we use SSL it’s reasonably safe to use.

According to RFC 2617 in order to use a basic authentication you have to add something that looks like the following header:

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

The highlighted goop of data is simply a base64encoding of: ”Aladdin:open sesame”, where “Aladdin” is the user name and “open sesame” is the password.

Nicely enough the HttpWebRequest class typically takes care of creating this header for us.

Encoding

When HttpWebReqeust creates the header for us it is using what’s called a Default Code Page Encoding, each 16 bit char in .NET is translated to an 8bit byte. If all the characters in the user name (or password for that matter) are recognized by the default code page then that operation succeeds.

But wait, let us say I typed the user name in German. If my local OS language is say German and my server OS language is English, the local translation of German chars will succeed, but when the credentials will arrive at the remote server they will fail to translate back.

If my local OS is English I will not be able to translate the chars at all, and will fail creating the HttpWebRequest.

Fortunately IIS7 supports authentication using the UTF-8 Encoding scheme, with UTF-8 we can encode without relying on the local OS code page.

Writing the new Header

So I thought that since we have direct access to the headers, we can just manually write a header and encode it using UTF-8. But now we have to deal with all sorts of scenarios where we will have to rewrite the headers and make sure they do not get overwritten by the HttpWebRequest code.

Fortunately .NET deals with that issue as well in an elegant way. The AuthenticationManager class exposes the Unregister and register method where we can register a class that will create the authentication headers when needed.

The AuthenticationManager being static (much like the better known sibling ServicePointManager) means that it cannot be directly used in the Web Deploy API level, and has to be part of the end user application.

First you need to create a class that will implement IAuthenticationModule. We will get to what it entails soon.

Somewhere in your app startup code you will unregister the default “Basic Authentication” module:

   1: AuthenticationManager.Unregister(“Basic”);

Then register the new class you just created:

   1: AuthenticationManager.Register(_myAuthenticationModule);

The key to the IAuthenticationModule is the creation of the credentials; we need to support this method on both the PreAuthenticate and Authenticate interfaces.

First we extract the NetworkCredential

   1: NetworkCredential nc = credentials.GetCredential(httpWebRequest.RequestUri, “Basic”);

Then create the basic ticket in plain text:

   1: string domain = nc.Domain;
   2:  
   3: string basicTicket = (!String.IsNullOrEmpty(domain) ? (domain + "\\") : "") +
   4:                      nc.UserName + ":" + nc.Password;

Now we encode it in UTF8 (this is the key to this whole change).

   1: byte[] bytes = Encoding.UTF8.GetBytes(basicTicket);

And encode it to a base64 string

   1: string header = AuthenticationTypeName + " " + Convert.ToBase64String(bytes);
   2:  
   3: return new Authorization(header, true);
   4:  

Summary and more information

Although there are lots of details in this solution, at the end of the day all the end user needs to do it compile the example class below into the existing application without modifying any other code. I thought that was kind of a neat solution.

This code will work for any framework that uses HttpWebRequest—not just Web Deploy—assuming the server side code can work with an UTF-8 encoded string.

I attached a full listing of an Authentication module, albeit a bit simplified for clarity, use at your own risk.

The topics covered in this blog are further expanded on MSDN and RFCs

RFC2617 –  HTTP Authentication: Basic and Digest Access Authentication

RFC4648 –  The Base16, Base32, and Base64 Data Encodings

Encoding in .NET - http://msdn.microsoft.com/en-us/library/system.text.encoding.aspx

Base64 in .NET - http://msdn.microsoft.com/en-us/library/dhx0d524.aspx

IAuthenticationModule - http://msdn.microsoft.com/en-us/library/system.net.iauthenticationmodule.aspx

 

A simplified full listing implementation of the AuthenticationModule

   1: using System;
   2: using System.Net;
   3: using System.Text;
   4:  
   5: namespace YourAppsNamespace
   6: {
   7:     internal class DeploymentAuthenticationModule : IAuthenticationModule {
   8:  
   9:         private const string AuthenticationTypeName = "Basic";
  10:         private static DeploymentAuthenticationModule _module = null;
  11:         private static object _lock = new object();
  12:  
  13:         public static void InstantiateIfNeeded()
  14:         {
  15:             lock (_lock)
  16:             {
  17:                 if (_module == null)
  18:                 {
  19:                     _module = new DeploymentAuthenticationModule();
  20:                 }
  21:             }
  22:         }
  23:  
  24:         private DeploymentAuthenticationModule()
  25:         {
  26:             AuthenticationManager.Unregister(AuthenticationTypeName);
  27:             AuthenticationManager.Register(this);
  28:         }
  29:  
  30:         string IAuthenticationModule.AuthenticationType {
  31:             get { return AuthenticationTypeName; }
  32:         }
  33:  
  34:         bool IAuthenticationModule.CanPreAuthenticate {
  35:             get { return true; }
  36:         }
  37:  
  38:         Authorization IAuthenticationModule.Authenticate(string challenge, WebRequest request, ICredentials credentials) {
  39:             HttpWebRequest httpWebRequest = request as HttpWebRequest;
  40:             if (httpWebRequest == null) {
  41:                 return null;
  42:             }
  43:  
  44:             // Verify that the challenge is a Basic Challenge
  45:             if (challenge == null || !challenge.StartsWith(AuthenticationTypeName, StringComparison.OrdinalIgnoreCase)) {
  46:                 return null;
  47:             }
  48:  
  49:             return Authenticate(httpWebRequest, credentials);
  50:         }
  51:  
  52:         Authorization IAuthenticationModule.PreAuthenticate(WebRequest request, ICredentials credentials) {
  53:             HttpWebRequest httpWebRequest = request as HttpWebRequest;
  54:  
  55:             if (httpWebRequest == null) {
  56:                 return null;
  57:             }
  58:  
  59:             return Authenticate(httpWebRequest, credentials);
  60:         }
  61:  
  62:         private Authorization Authenticate(HttpWebRequest httpWebRequest, ICredentials credentials) {
  63:             if (credentials == null) {
  64:                 return null;
  65:             }
  66:  
  67:             // Get the username and password from the credentials
  68:             NetworkCredential nc = credentials.GetCredential(httpWebRequest.RequestUri, AuthenticationTypeName);
  69:             if (nc == null) {
  70:                 return null;
  71:             }
  72:  
  73:             ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
  74:             if (policy != null && !policy.ShouldSendCredential(httpWebRequest.RequestUri, httpWebRequest, nc, this)) {
  75:                 return null;
  76:             }
  77:  
  78:             string domain = nc.Domain;
  79:  
  80:             string basicTicket = (!String.IsNullOrEmpty(domain) ? (domain + "\\") : "") + nc.UserName + ":" + nc.Password;
  81:             byte[] bytes = Encoding.UTF8.GetBytes(basicTicket);
  82:             
  83:             string header = AuthenticationTypeName + " " + Convert.ToBase64String(bytes);
  84:             return new Authorization(header, true);
  85:         }
  86:     }
  87: }

1 Comment

Comments have been disabled for this content.