OWIN and Katana - Creating an authentication middleware
In this article I will focus on the creation of an authentication middleware for Katana (the implementation by Microsoft of OWIN) taking as an example a middleware allowing to authenticate a user using the HTTP Basic method. 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:
public abstract Task Invoke(IOwinContext context);
Authentication often relies on the same mechanisms, so Microsoft introduced the AuthenticationMiddleware class (which inherits from OwinMiddleware
)
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 very interesting in itself but it guides us in the implementation: we must create an AuthenticationHandler
. This class is instantiated for each query and contains four main methods. The first two are called during the processing of the request, while the other two are called when sending the response.
AuthenticateCoreAsync
Used to authenticate the user using the content of the request (header, 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 if the Middleware is considered active. In the case of passive authentication, the Middleware is called only when needed, usually by the application at the application level using the AuthenticationManager.InvokeAsync
The purpose of this method is to determine if the request corresponds to the "return" of an authentication (as it might be the case with OpenID or OAuth). If this is the case the method must authenticate the user (often using another middleware such as
CookieAuthenticationMiddleware
), redirect it to the correct URL and returntrue
to indicate that no other Middleware should be called. Otherwise just returnfalse
.ApplyResponseGrantAsync
This method allows you to add or remove a cookie, token, or other in the response. For example, in the case of a cookie authentication, this method makes it possible to add it after the authentication and to remove it following a disconnection.
ApplyResponseChallengeAsync
This method is mainly used to handle responses with code 401 (unauthorized). At this time according to the authentication method, it is possible to modify the response to add a challenge. For example for HTTP Basic authentication, we can add the header
WWW-Authenticate
.
We now have all the cards to create the Middleware. Let's start with the simplest part:
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")
{
}
}
Now you have to create the Handler:
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, you must register it and define the method to check the user. To stay up-to-date, the chosen method uses ASP.NET Identity:
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!