Authentication - Session, Basic and OAuth

Authentication

Authenticating users of an API is very important - and thankfully the many available extension points within ASP.Net MVC WebAPI make this really easy to implement.

We intended the API being developed to be consumed by both the application itself, as well as clients armed with a username and password - so that meant:

  • Session based authentication
  • Basic Authentication

The application was already an OAuth provider (to support our OpenSocial gadget support) - so we also decided to adopt this for the API, thus allowing those gadgets to also interact with the API (and to allow for delegated authentication scenarios).

Delegating Handler

Initially we attempted to support these three authentication methods through separate delegating handlers, but eventually abandoned that approach for a single class that handled all 3 authentication methods - here is the guts of determining which method to use:

Task AuthenticateRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Properties.ContainsKey("user") && request.Properties["user"] != null)
{
return HandlePreAuthenticated(request, cancellationToken);
}

var context = request.GetHttpContext();

if (request.Headers != null
&& request.Headers.Authorization != null
&& request.Headers.Authorization.Scheme != null)
{
if (request.Headers.Authorization.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase))
{
return HandleWithBasicAuthAuthentication(request, cancellationToken);
}

if (request.Headers.Authorization.Scheme.Equals("OAuth", StringComparison.OrdinalIgnoreCase))
{
return HandleWithOAuthAuthentication(request, cancellationToken, context);
}

return Task.Factory.StartNew(() => new HttpResponseMessage(HttpStatusCode.Unauthorized));
}

return HandleWithSessionAuthentication(request, cancellationToken, context);
}

So the approach taken was that:

  • If the request properties contains a user, we treat the request as pre-authenticated (used for testing mostly, more on that in a future post).
  • If there is authorization header, we check the scheme and perform either Basic or OAuth handling of the request.
  • Otherwise, we fall through to handling the request with session authentication.

The HttpContext (and it's related abstractions) are fairly baked in to parts of the pre-existing Authentication infrastructure and so we need to extract this from the request to complete authentication in many cases - this has actually got much easier with each release of the WebAPI - the first WCF based drops of the WebAPI made this almost impossible to do without spelunking into reflection over private fields.

All authentication methods would eventually end up associating an authenticated user with the requests properties via a SetIdentity method:

void SetIdentity(User user, HttpRequestMessage request)
{
request.Properties.Add("user", user);
}

OAuth

Enterprise Tester uses DevDefined.OAuth - which includes support for the problem reporting extension as part of OAuth 1 - this is exposed as Report property on the OAuthException, which can be then used as the content of a response when Authentication fails:

Task HandleWithOAuthAuthentication(
HttpRequestMessage request,
CancellationToken cancellationToken,
HttpContextBase context)
{
var httpRequest = context.Request;

try
{
User user = _authenticationService.AuthenticateRequest(httpRequest);

SetIdentity(user, request);

return base.SendAsync(request, cancellationToken);
}
catch (OAuthException authEx)
{
string reportAsText = authEx.Report.ToString();

if (Logger.IsErrorEnabled) Logger.ErrorFormat(authEx, "OAuth Error occurred while authenticating OAuth request, url: {0}, method: {1}", httpRequest.Url, httpRequest.HttpMethod);

return Task.Factory.StartNew(() => new HttpResponseMessage(HttpStatusCode.Forbidden) {Content = new StringContent(reportAsText)});
}
catch (Exception ex)
{
if (Logger.IsErrorEnabled) Logger.ErrorFormat(ex, "General Error occurred while authenticating OAuth request, url: {0}, method: {1}", httpRequest.Url, httpRequest.HttpMethod);

var report = new OAuthProblemReport {Problem = OAuthProblems.PermissionUnknown, ProblemAdvice = "Encountered general error: " + ex.Message + " - please see application logs for more details"};

string reportAsText = report.ToString();

return Task.Factory.StartNew(() => new HttpResponseMessage(HttpStatusCode.Forbidden) {Content = new StringContent(reportAsText)});
}
}

Async

Within the application we have a simple service for returning the "current user" associated with a request/thread:

public interface IUserContext
{
User CurrentUser { get; }
}

With the WebAPI being asynchronous the mechanics of this didn't work very well for us (The thread the DelegatingHandler executes on for Authentication wasn't necessarily the same thread that constructed the controller and executed the action).

To avoid too much rework we just implemented an ActionFilterAttribute that was applied to a base controller which all the "authenticated" controllers inherited from:

public class AssociateUserWithThreadFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var service = IoC.Resolve();

var user = actionContext.Request.Properties["user"] as User;

if (user != null && service != null)
{
service.SetIdentity(user, Authorization.Everything);
}

base.OnActionExecuting(actionContext);
}
}

It's not a beautiful solution, but had no impact on our existing implementation for authentication which is great.

The one gotcha here is if returning a Task as the result of a controller action you need to be a little careful/take care with associating the user with the task's thread yourself.

So far we only return a Task from the POST methods handling the upload of attachments as a mime multipart request, so this hasn't been too much of a problem to deal with.

Next

Next in part 6 we take a look at how we handled exposing long running tasks.

Written on August 20, 2012