[C#] Authentication basics and Custom Authentication Scheme
- siqisundev
- Nov 21, 2022
- 5 min read
Updated: Nov 23, 2022
Term Disambiguation
Conventionally, Authentication and Authorisation are respectively defined as:
Authentication: Proving the tenant's identity (i.e. prove who you are)
Authorisation: Verifying the rights of the tenant (i.e. show what you can do)
The definition also claimed that the two terms should be orthogonal. In the programming domain context, when two terms are orthogonal, they are:
Related but irreplaceable to each other
Together they achieve a goal / implement a feature
This means the two terms can not be used interchangeably, which might be counterintuitive especially for non-native English speakers (like myself 🙃)
This also means when it comes to implementing protection of resources, the two components need to be implemented separately and in a self-contained manner. (doing this also brings the goodness of Single Responsibility Principle)
The process of Authentication and Authorisation in .NET follows the above "conventional" definition. It has a comprehensive built-in implementation of the Authentication and Authorisation while allowing maximum level of customisation on the rules. This post will focus on the Authentication part by investigating the component. And enhance the understanding by implementing a custom Authentication Scheme. We have another post which dives into authorization component.
Authentication Components
Components
In .NET, authentication feature is provided by built-in AuthenticationService class implementing IAuthenticationService interface. The AuthenticationService is able to provide relevant AuthenticationHandlers, where the actual authentication logic sits. The registered authentication handlers and their configuration options (the AuthenticationSchemeOptions base class) are called "schemes", the scheme (i.e. the combination of the handler and option objects) are identified using simple strings of the scheme names. In the most common web based authentication scenario, the AuthenticationService class will be used by the AuthenticationMiddleware (more on the official document for .NET middleware).
All relevant services are added to the dependency injection containers by using the 'AddAuthentication' methods (example code assumes the application is built upon minimum webHost template from .NET 6+, the way for registering the services in more traditional components is almost the same though).
builder.Services.AddAuthentication(
options => options.DefaultAuthenticationScheme
= "MyCustomAuthScheme"
)/*Registers all the services relevant to the built in AuthenticationService, set default scheme to be "MyCustomAuthScheme" */
.AddScheme<MyCustomAuthConfiguration, MyCustomHttpHeaderAuthHandler>(
"MyCustomAuthScheme",
options => {
options = new MyCustomAuthConfiguration(){/*Ignore initializations there*/}
});/*Add an auth scheme with MyCustomAuthConfiguration and the Handler, registered configuration instance is set in the option delegate*/
app.UseAuthentication(); //add the middleware
Feature and options of auth handlers
Features
The authentication handlers has access to the HTTP context and is supposed to provide Authentication, Challenge and Forbid services, which respectively refers to:
Authentication: Verifying users identity (as mentioned). Implementation wise the Authentication method should be deserialising the relevant request information (e.g. the bearer token) into the user identity object. So that the downstream middleware and endpoints can use that.
Challenge: Respond to the requests WITHOUT identity component. Implementation wise the Challenge method should be tweaking the response content accordingly e.g. setting the Redirect in the response to redirect user to authentication page.
Forbid: Respond to the requests where the identity do not have the privilege to view particular resources. Example implementation method is to change the response status code to 403 forbidden.
Note: Challenge and Forbid are service provided by Authentication handler but does not strictly fit in the 'Authentication' domain. Rather it's a service provided to Authorisation handlers. Namely it's the authorisation service calling the Challenge and Forbid services provided by the AuthenticationHandler and this is where this orthogonal component joins. I will talk more on this in future posts where we discuss Authorisation.
Options
On the authentication service level, the options object can configure the default scheme to be used for different services, it can also specify schemes to be used for specific operations such as mentioned challenge, forbid etc.
On each individual scheme level, the base AuthenticationSchemeOptions class provide default available options to forward Forbid/Challenge (and other services) to other schemes. This class can be inherited and extended to have any custom configurations, in the next section where we implement our own custom authentication scheme, we will have the extended option to tell the handlers to check for a particular HTTP handler for authentication information.
Custom Authentication Scheme
As inspected above, to implement a custom authentication scheme, all we need are:
An authentication handler inheriting the IAuthenticationHandler interface or the AuthenticationHandler<TOptions> generic base class
The TOptions class extending the scheme option to be used by the custom authentication handler.
The way to register the handler and options is already shown in the above section.
We will implement a CustomHttpHeaderAuthHandler, all it does is to check if a particular http header contains a magic keyword. The name of the header and magic keyword can be configured.
As such, to allow the configuration, we need an option class that extends the AuthenticationSchemeOption class for the extra configuration of httpHeader name and the magic keyword.
public class CustomHttpHeaderAuthConfiguration : AuthenticationSchemeOptions
{
public string HeaderName { get; set; } = string.Empty;
public string? MustContain { get; set; }
}
The authentication handler using the CustomHttpHeaderAuthConfiguration can then be declared by extending the generic base class as follows
public class CustomHttpHeaderAuthHandler
: AuthenticationHandler<CustomHttpHeaderAuthConfiguration>
{
public CustomHttpHeaderAuthHandler(
IOptionsMonitor<CustomHttpHeaderAuthConfiguration> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
}
The only mandatory method to implement is the authentication method. The inherited Request field is set by the pipeline and the method will investigate the specific header name and compare with the keyword specified by the option.
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey(Options.HeaderName))
{
return AuthenticateResult.NoResult();
}
if (!Request.Headers[Options.HeaderName].ToString()
.Contains(Options.MustContain, StringComparison.CurrentCultureIgnoreCase))
return AuthenticateResult.Fail("No Magic Word");
var claims =
new Claim[]
{
new Claim(
"MagicWord",
Request.Headers[Options.HeaderName].ToString()
)
};
return AuthenticateResult.Success(
new AuthenticationTicket(
new ClaimsPrincipal(
new ClaimsIdentity(claims, this.Scheme.Name)
),
Scheme.Name
)
);
}
Upon successful authentication, the authentication result should return an authentication ticket containing set of claims for the identity, to be used in the downstream pipeline and endpoints (in particular, the authorisation pipeline). This is how the authentication handler deserialise the request content into the identity objects.
Caveats
Note that we have a NoResult and Fail for the AuthenticateResults, where NoResults indicates no identity is provided and Fail indicates failure in verifying users identity, which will wrap an exception with messages ("no magic keyword" in the example) that could be raised in the downstream codes. In our custom authentication handlers, verification fails if the magic word is not included in the users identity. The identity verification rule should be much more comprehensive and secure. Such as the .NET built in JwtAuthentication handler which checks the signing key of the JWT issuer. In the future posts we will discuss the usages of some built in Authentication handlers.
It's worth mentioning that the authentication method should focus on the authentication logics and exclude authorisation logics due to the independent nature of the 2 components and the Single Responsibility principle. A common misuse for developers new to the area is to return the Fail result if the deserialised identity is missing a claim etc., which is essentially an authorisation failure other then authentication failure.
Comments