Interestingly enough, while I was writing the last comment regarding the answer above, I stopped for a moment. My comment contained "...If the HTTP header doesn't contain the "Authorization" header, I set the status to 401, which causes the exception." I set the status to 401. Got it? The solution was there all along.
The initial packet doesn't contain the authorization header even if I explicitly add it. However each consequent packet does contain it as I have tested while having the authorization module inactive. So I though, why don't I try to distinguish this initial packet from the others? So if I see that it's the initial packet, set HTTP status code to 200 (OK), and if it's not - check for authentication header. That was easy, since the initial packet sends a request for the security token in a SOAP envelope (Contains <t:RequestSecurityToken>
tags).
Ok so let's take a look at my implementation, in case someone else would need it.
This is the BasicAuthenticationModule implementation, which implements IHTTPModule:
public class UserAuthenticator : IHttpModule
{
public void Dispose()
{
}
public void Init(HttpApplication application)
{
application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest);
application.EndRequest += new EventHandler(this.OnEndRequest);
}
public void OnAuthenticateRequest(object source, EventArgs eventArgs)
{
HttpApplication app = (HttpApplication)source;
// Get the request stream
Stream httpStream = app.Request.InputStream;
// I converted the stream to string so I can search for a known substring
byte[] byteStream = new byte[httpStream.Length];
httpStream.Read(byteStream, 0, (int)httpStream.Length);
string strRequest = Encoding.ASCII.GetString(byteStream);
// This is the end of the initial SOAP envelope
// Not sure if the fastest way to do this but works fine
int idx = strRequest.IndexOf("</t:RequestSecurityToken></s:Body></s:Envelope>", 0);
httpStream.Seek(0, SeekOrigin.Begin);
if (idx != -1)
{
// Initial packet found, do nothing (HTTP status code is set to 200)
return;
}
//the Authorization header is checked if present
string authHeader = app.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authHeader))
{
if (authHeader == null || authHeader.Length == 0)
{
// No credentials; anonymous request
return;
}
authHeader = authHeader.Trim();
if (authHeader.IndexOf("Basic", 0) != 0)
{
// the header doesn't contain basic authorization token
// we will pass it along and
// assume someone else will handle it
return;
}
string encodedCredentials = authHeader.Substring(6);
byte[] decodedBytes = Convert.FromBase64String(encodedCredentials);
string s = new ASCIIEncoding().GetString(decodedBytes);
string[] userPass = s.Split(new char[] { ':' });
string username = userPass[0];
string password = userPass[1];
// the user is validated against the SqlMemberShipProvider
// If it is validated then the roles are retrieved from
// the role provider and a generic principal is created
// the generic principal is assigned to the user context
// of the application
if (Membership.ValidateUser(username, password))
{
string[] roles = Roles.GetRolesForUser(username);
app.Context.User = new GenericPrincipal(new
GenericIdentity(username, "Membership Provider"), roles);
}
else
{
DenyAccess(app);
return;
}
}
else
{
app.Response.StatusCode = 401;
app.Response.End();
}
}
public void OnEndRequest(object source, EventArgs eventArgs)
{
// The authorization header is not present.
// The status of response is set to 401 Access Denied.
// We will now add the expected authorization method
// to the response header, so the client knows
// it needs to send credentials to authenticate
if (HttpContext.Current.Response.StatusCode == 401)
{
HttpContext context = HttpContext.Current;
context.Response.AddHeader("WWW-Authenticate", "Basic Realm");
}
}
private void DenyAccess(HttpApplication app)
{
app.Response.StatusCode = 403;
app.Response.StatusDescription = "Forbidden";
// Write to response stream as well, to give the user
// visual indication of error
app.Response.Write("403 Forbidden");
app.CompleteRequest();
}
}
Important: in order for us to be able to read the http request stream, ASP.NET compatibility must not be enabled.
To make your IIS load this module, you must add it to <system.webServer>
section of web.config, like this:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<remove name="BasicAuthenticationModule" />
<add name="BasicAuthenticationModule" type="UserAuthenticator" />
</modules>
But before that, you must ensure BasicAuthenticationModule
section is not locked, and it should be locked by default. You will not be able to replace it if it's locked.
To unlock the module: (note: I am using IIS 7.5)
- Open IIS Manager
- In the left pane, click on your host name
- In the middle pane, under "Management" section, open "Configuration Editor"
- Click the combo box next to "Section" label in the upper pane section, expand "system.webServer" then navigate to "modules"
- Under "(Collection)" key, click "(Count=nn)" value to have a small button with "..." appear. Click on it.
- In the "Items" list, find "BasicAuthenticationModule" and in the right pane click "Unlock Item" (if present!).
- If you changed this setting, close the Configuration Editor, saving changes.
On the client side, you need to be able to add custom HTTP headers to the outgoing message. The best way to do this is to implement IClientMessageInspector and add your headers using the BeforeSendRequest
function. I will not explain how to implement IClientMessageInspector, there are plenty of resources on that topic available online.
To add the "Authorization" HTTP header to the message, do the following:
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
// Making sure we have a HttpRequestMessageProperty
HttpRequestMessageProperty httpRequestMessageProperty;
if (request.Properties.ContainsKey(HttpRequestMessageProperty.Name))
{
httpRequestMessageProperty = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
if (httpRequestMessageProperty == null)
{
httpRequestMessageProperty = new HttpRequestMessageProperty();
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty);
}
}
else
{
httpRequestMessageProperty = new HttpRequestMessageProperty();
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty);
}
// Add the authorization header to the WCF request
httpRequestMessageProperty.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(Service.Proxy.ClientCredentials.UserName.UserName + ":" + Service.Proxy.ClientCredentials.UserName.Password)));
return null;
}
There ya go, it took a while to resolve but it was worth it, as I found many similar unanswered questions throughout the web.