Condividi tramite


OData and Authentication – Part 6 – Custom Basic Authentication

You might remember, from Part 5, that Basic Authentication is built-in to IIS.

So why do we need ‘Custom’ Basic Authentication?

Well if you are happy using windows users and passwords you don’t.

That’s because the built-in Basic Authentication, uses the Basic Authentication protocol, to authenticate against the windows user database.

If however you have a custom user/password database, perhaps it’s part of your application database, then you need ‘Custom’ Basic Authentication.

How does basic auth work?

Basic authentication is a very simple authentication scheme, that should only be used in conjunction with SSL or in scenarios where security isn’t paramount.

If you look at how a basic authentication header is fabricated, you can see why it is NOT secure by itself:

var creds = "user" + ":" + "password";
var bcreds = Encoding.ASCII.GetBytes(creds);
var base64Creds = Convert.ToBase64String(bcreds);
authorizationHeader = "Basic " + base64Creds;

Yes that’s right the username and password are Base64 encoded and shipped on the wire for the whole world to see, unless of course you are also using SSL for transport level security.

Nevertheless many systems use basic authentication. So it’s worth adding to your arsenal.

Server Code:

Creating a Custom Basic Authentication Module:

Creating a Custom Basic Authentication module should be no harder than cracking Basic Auth, i.e. it should be child’s play.

We can use our HttpModule from Part 5 as a starting point:

public class BasicAuthenticationModule: IHttpModule
{
public void Init(HttpApplication context)
{
context.AuthenticateRequest
           += new EventHandler(context_AuthenticateRequest);
}
void context_AuthenticateRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
if (!BasicAuthenticationProvider.Authenticate(application.Context))
{
application.Context.Response.Status = "401 Unauthorized";
application.Context.Response.StatusCode = 401;
application.Context.Response.AddHeader("WWW-Authenticate", "Basic");
application.CompleteRequest();
}
}
public void Dispose() { }
}

The only differences from Part 5 are:

  • We’ve changed the name to BasicAuthenticationModule.
  • We use a new BasicAuthenticationProvider to do the authentication.
  • And if the logon fails we challenge using the “WWW-Authenticate” header.

The final step is vital because without this clients that don’t send credentials by default – like HttpWebRequest and by extension DataServiceContext – won’t know to retry with the credentials when their first attempt fails.

Implementing the BasicAuthenticationProvider:

The Authenticate method is unchanged from our example in Part 5:

public static bool Authenticate(HttpContext context)
{
if (!HttpContext.Current.Request.IsSecureConnection)
return false;

    if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization"))
return false;

    string authHeader = HttpContext.Current.Request.Headers["Authorization"];

    IPrincipal principal;
if (TryGetPrincipal(authHeader, out principal))
{
HttpContext.Current.User = principal;
return true;
}
return false;
}

Our new TryGetPrincipal method looks like this:

private static bool TryGetPrincipal(string authHeader, out IPrincipal principal)
{
var creds = ParseAuthHeader(authHeader);
if (creds != null && TryGetPrincipal(creds, out principal))
return true;

    principal = null;
return false;
}

As you can see it uses ParseAuthHeader to extract the credentials from the authHeader – so they can be checked against our custom user database in the other TryGetPrincipal overload:

private static string[] ParseAuthHeader(string authHeader)
{
// Check this is a Basic Auth header
if (
authHeader == null ||
authHeader.Length == 0 ||
!authHeader.StartsWith("Basic")
) return null;

    // Pull out the Credentials with are seperated by ':' and Base64 encoded
string base64Credentials = authHeader.Substring(6);
string[] credentials = Encoding.ASCII.GetString(
Convert.FromBase64String(base64Credentials)
).Split(new char[] { ':' });

if (credentials.Length != 2 ||
string.IsNullOrEmpty(credentials[0]) ||
string.IsNullOrEmpty(credentials[0])
) return null;

    // Okay this is the credentials
return credentials;
}

First this code checks that this is indeed a Basic auth header and then attempts to extract the Base64 encoded credentials from the header.

If everything goes according to plan the array returned will have two elements: the username and the password.

Next we check our ‘custom’ user database to see if those credentials are valid.

In this toy example I have it completely hard coded:

private static bool TryGetPrincipal(string[] creds,out IPrincipal principal)
{
if (creds[0] == "Administrator" && creds[1] == "SecurePassword")
{
principal = new GenericPrincipal(
new GenericIdentity("Administrator"),
new string[] {"Administrator", "User"}
);
return true;
}
else if (creds[0] == "JoeBlogs" && creds[1] == "Password")
{
principal = new GenericPrincipal(
new GenericIdentity("JoeBlogs"),
new string[] {"User"}
);
return true;
}
else
{
principal = null;
return false;
}
}

You’d probably want to check a database somewhere, but as you can see that should be pretty easy, all you need is a replace this method with whatever code you want.

Registering our BasicAuthenticationModule:

Finally you just need to do is add this to your WebConfig:

<system.webServer>
<modules>
<add name="BasicAuthenticationModule"
type="SimpleService.BasicAuthenticationModule"/>
</modules>
</system.webServer>

Allowing unauthenticated access:

If you want to allow some unauthenticated access to your Data Service, you could change your BasicAuthenticationModule so it doesn’t ‘401’ if the Authenticate() returns false.

Then if certain queries or updates actually require authentication or authentication, you could check HttpContext.Current.Request.IsAuthenticated or HttpContext.Current.Request.User in QueryInterceptors and ChangeInterceptors as necessary.

This approach allows you to mix and match your level of security.

See part 4 for more on QueryInterceptors.

Client Code:

When you try to connect to an OData service protected with Basic Authentication (Custom or built-in) you have two options:

Using the DataServiceContext.Credentials:

You can use a Credentials Cache like this.

var serviceCreds = new NetworkCredential("Administrator", "SecurePassword");
var cache = new CredentialCache();
var serviceUri = new Uri("https://localhost/SimpleService");
cache.Add(serviceUri, "Basic", serviceCreds);
ctx.Credentials = cache;

When you do this the first time Data Services attempts to connect to the Service the credentials aren’t sent – so a 401 is received.

However so long as the service challenges using the "WWW-Authenticate" response header, it will seamlessly retry under the hood.

Using the request headers directly:

Another option is to just create and send the authentication header yourself.

1) Hook up to the DataServiceContext’s SendingRequest Event:

ctx.SendingRequest +=new EventHandler<SendingRequestEventArgs>(OnSendingRequest);

2) Add the Basic Authentication Header to the request:

static void OnSendingRequest(object sender, SendingRequestEventArgs e)
{
var creds = "user" + ":" + "password";
var bcreds = Encoding.ASCII.GetBytes(creds);
var base64Creds = Convert.ToBase64String(bcreds);
e.RequestHeader.Add("Authorization", "Basic " + base64Creds);
}

As you can see this is pretty simple. And has the advantage that it will work even if the server doesn’t respond with a challenge (i.e. WWW-Authenticate header).

Summary:

You now know how to implement Basic Authentication over a custom credentials database and how to interact with a Basic Authentication protected service using the Data Service Client.

Next up we’ll look at Forms Authentication in depth.

Alex James
Program Manager
Microsoft.

Comments

  • Anonymous
    July 21, 2010
    Quite a popular custom basic auth module (including IIS manager integration etc) can be found atcustombasicauth.codeplex.com

  • Anonymous
    July 29, 2010
    Did you try this with non .net client (ie. PHP) ?

  • Anonymous
    October 06, 2010
    Thank you, very useful!Little typing error:e.RequestHeader.Add("Auth.....  ===>  e.RequestHeaders.Add(....)'S' at the end of Header.Best regardswww.notafiscaldeservicos.com.br - http://www.virtualgroup.com.br

  • Anonymous
    October 08, 2010
    hi,Could you please tell me if it would work if I  encrypted the string in the header instead of encoding it as a base64 string?Thanks

  • Anonymous
    June 14, 2011
    You can trash my earlier comment.  I was instantiating the context in the BasicAuthenticationProvider.TryGetPrincipal outside of a using block without calling dispose.

  • Anonymous
    November 19, 2012
    Need help, We have a project of WCF Services with basic authentication. Our client is PERL user and he pass credentials every time for every WCF service. Is there any mechanism that user passes credential for the first time only, for rest of the call there will be no credentials(e.g time out is 20 mins etc) Thanks a lot. Kamran.

  • Anonymous
    November 12, 2013
    Thanks you for the very useful article! I also wanted to point out a possible bug in your code for future readers: <code>if (credentials.Length != 2 ||        string.IsNullOrEmpty(credentials[0]) ||        string.IsNullOrEmpty(credentials[0])    ) return null; </code> This should check credentials[0] && credentials[1]