OWIN and Katana - Creating an authentication middleware

 
 
  • Gérald Barré

In this article, I focus on creating an authentication middleware for Katana (Microsoft's implementation of OWIN), using HTTP Basic authentication as the example. This authentication method is described in a previous article.

To create a Middleware you have to create a class inheriting from OwinMiddleware. This class is very general and has only one method:

C#
public abstract Task Invoke(IOwinContext context);

Authentication often relies on the same mechanisms, so Microsoft introduced the AuthenticationMiddleware class (which inherits from OwinMiddleware)

C#
public abstract class AuthenticationMiddleware<TOptions>
   : OwinMiddleware where TOptions : AuthenticationOptions
{
    protected AuthenticationMiddleware(OwinMiddleware next, TOptions options)
    public TOptions Options { get; set; }
    public override async Task Invoke(IOwinContext context);
    protected abstract AuthenticationHandler<TOptions> CreateHandler();
}

This class is not particularly interesting on its own, but it guides the implementation: we must create an AuthenticationHandler. This class is instantiated for each request and contains four main methods. The first two are called during request processing, while the other two are called when sending the response.

  • AuthenticateCoreAsync

    Used to authenticate the user based on the content of the request (headers, URL, cookies, etc.). Returns an AuthenticationTicket, which contains an Identity property of type ClaimsIdentity, or null if the user is not authenticated. This method is called automatically when the middleware is considered active. For passive authentication, the middleware is called only when needed, typically at the application level via the AuthenticationManager.

  • InvokeAsync

    This method determines whether the request corresponds to an authentication callback (as is common with OpenID or OAuth). If so, it must authenticate the user (often using another middleware such as CookieAuthenticationMiddleware), redirect to the correct URL, and return true to indicate that no further middleware should be called. Otherwise, return false.

  • ApplyResponseGrantAsync

    This method lets you add or remove a cookie, token, or similar value in the response. For example, with cookie authentication, it adds the cookie after a successful sign-in and removes it on sign-out.

  • ApplyResponseChallengeAsync

    This method is primarily used to handle 401 (Unauthorized) responses. Depending on the authentication method, the response can be modified to include a challenge. For example, with HTTP Basic authentication, we can add the WWW-Authenticate header.

We now have everything needed to create the middleware. Let's start with the simplest part:

C#
public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
    public BasicAuthenticationMiddleware(OwinMiddleware next, BasicAuthenticationOptions options)
        : base(next, options)
    {
    }

    protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
    {
        return new BasicAuthenticationHandler();
    }
}

public delegate Task<AuthenticationTicket> ValidateCredentialHandler(IOwinContext context, string userName, string password);

public class BasicAuthenticationOptions : AuthenticationOptions
{
    public ValidateCredentialHandler ValidateCredentials { get; set; }
    public string Realm { get; set; }

    public BasicAuthenticationOptions()
        : base("Basic")
    {
    }
}

Next, create the handler:

C#
public class BasicAuthenticationHandler : AuthenticationHandler
{
    protected override Task AuthenticateCoreAsync()
    {
        if (Options.ValidateCredentials == null)
            throw new InvalidOperationException("ValidateCredential must be set.");

        var authorizationHeaderValue = Request.Headers.Get("Authorization");
        AuthenticationHeaderValue authenticationValue;
        if (AuthenticationHeaderValue.TryParse(authorizationHeaderValue, out authenticationValue))
        {
            if (string.Equals(authenticationValue.Scheme, "basic", StringComparison.OrdinalIgnoreCase))
            {
                return ValidateHeader(authenticationValue.Parameter);
            }
        }

        return Task.FromResult((AuthenticationTicket)null);
    }

    protected override Task ApplyResponseChallengeAsync()
    {
        if (Response.StatusCode == 401)
        {
            Response.Headers.Append("WWW-Authenticate", "Basic realm=" + (Options.Realm ?? GetRealm()));
        }

        return Task.FromResult<object>(null);
    }

    protected virtual Task<AuthenticationTicket> ValidateHeader(string authHeader)
    {
        // Decode the authentication header & split it
        var fromBase64String = Convert.FromBase64String(authHeader);
        var lp = Encoding.Default.GetString(fromBase64String);
        if (string.IsNullOrWhiteSpace(lp))
            return null;

        string login;
        string password;
        int pos = lp.IndexOf(':');
        if (pos < 0)
        {
            login = lp;
            password = string.Empty;
        }
        else
        {
            login = lp.Substring(0, pos).Trim();
            password = lp.Substring(pos + 1).Trim();
        }

        Task<AuthenticationTicket> result = Options.ValidateCredentials(Context, login, password);
        if (result == null)
            return Task.FromResult((AuthenticationTicket)null);
        return result;
    }
}

To use the middleware, register it and provide a credential validation callback. The example below uses ASP.NET Identity:

C#
public void ConfigureAuth(IAppBuilder app)
{
    var basicAuthenticationOptions = new BasicAuthenticationOptions();
    basicAuthenticationOptions.ValidateCredentials = (context, userName, password) =>
    {
        var userManager = context.GetUserManager<UserManager<User>>();
        if (userManager == null)
            return null;

        var user = userManager.FindByName(userName);
        if (user == null)
            return null;

        var userId = ((IUser)user).Id;
        var result = userManager.CheckPassword(user, password);
        if (result)
        {
            return Task.FromResult(new AuthenticationTicket(userManager.CreateIdentity(user, "Basic"), null));
        }

        return null;
    };

    app.Use(typeof(BasicAuthenticationMiddleware), basicAuthenticationOptions);
}

And voilà 😃

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?