Introduction
In an enterprise application scenario, securing application from different security threats is very important, to begin with providing secured authorization and authorization is the main part of the application. In this article, I will share basic knowledge of security features used in ASP.NET Core.
Background
In this section, I would like to brief about basic security types for the client-server application. In client-server application, two types of security are very important, one is data transport and data access.
1. Transport Security
A simple way to understand transport security is with the customer and bank scenario. example customer is the client/browser and the bank is the web server. In this scenario, it's easy to understand how the customer will do cash transactions with the bank.
Http: example - assume customer carrying money to a bank, the customer will use the different transportation, i.e., bus, train, and car facilities to reach the bank and carrying money in a transparent polythene bag. The customer will use many transportation facilities but carrying money in a polythene bag is a risk (anyone can see and steal money from your bag, there is no security provided to customer bag).
Https: example - the customer has to use the same transportation (as described above) but money is securely locked in a bag, the customer will lock the bag with key (provided by the bank) and the only bank can open this bag with bank personal key. Even if the customer lost the bag, no one can open or steal money from the bag (i.e., the bank has the personal key to open the locked bag).
How Http Works?
Who is Responsible for the Secured Transaction?
Client (Web server) will request to the server (Web Server) to do specific operations and the server will respond to the client request. In this scenario, both have to make sure secure communication. so it's both client and server responsibility to provide security.
How to Make a Secured Transaction?
Communication between client to server and server to the client is secured only with HTTPS (HyperText Transport Protocol Secure). As explained, the customer carries money with polythene bag is not secure as anyone can see your money and there is risk of theft. But in HTTPS, you are carrying money with security box - even though if someone steals your bag, she/he cannot open the box. HTTPS uses SSL certificates to secure the transactions are securely encrypted.
How SSL Works?
http://www.jordansphere.co.uk/how-does-ssl-work/
What is Secured in HTTPS?
Example HTTP Message:
Example HTTPS Message:
Who is Responsible for Encrypting and Decryption of SSL at Client Side
At the client, Browser is responsible for encryption and decryption of SSL certificate.
The browser will encrypt and decrypt data using SSL certifications, browser makes sure data is encrypted sent via network channels. once server (i.e., IIS, Apache) receives data, the server has the responsibility to make sure the data is encrypted (secured) and responsible for sending secured response.
2. Application Security
Application security is what resources can be used in the application (Authorization) is determined by who is going to use the application (Authentication) providing application security both authorization and authentication required.
- Identification of User - Authentication
- Providing resource access to user - Authorization
Example: Consider a public library, the user can enter into the public library and use library resources (newspaper, books, videos, computers, etc.) by providing identity just to know the user information. Proving Identity is Authentication and permission to access library resource is Authorization.
ASP.NET Core Authentication
ASP.NET core authentication deals with mainly three parts:
- The
AuthenticationMiddleware
- The
AuthenticationSchemeOptions
- The
AuthenticationHandler
AuthenticationMiddleware
will intercept the pipeline and authenticates the request. To enable AuthenticationMiddleware
, use UseAuthentication()
function in the Startup
class:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
}
This will add the AuthenticationMiddleware
to the specified ApplicationBuilder
. The default builder is created at initialization of the application.
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
app.UseAuthentication();
will create new AuthenticationMiddleware
middleware, AuthenticationMiddleware
handles request early in the pipeline and validates the authentication.
One thing to remember about this feature is that unless proving Authorization, this middleware authentication interceptor will not restrict access to any of the resources.
AuthenticationSchemeOptions
will validate the different options provided by the scheme it will be used by AuthenticationHandler
. In custom authentication, this class can be derived to provide more options for validated different parameters based on requirements.
public class AuthenticationSchemeOptions
{
public virtual void Validate() { }
public virtual void Validate(string scheme)
=> Validate();
public string ClaimsIssuer { get; set; }
public object Events { get; set; }
public Type EventsType { get; set; }
}
The below class CookieAuthenticationOptions
derived from AuthenticationSchemeOptions
and provided with many properties and events.
public class CookieAuthenticationOptions : AuthenticationSchemeOptions
{
public CookieAuthenticationOptions()
{
ExpireTimeSpan = TimeSpan.FromDays(14);
ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
SlidingExpiration = true;
Events = new CookieAuthenticationEvents();
}
public CookieBuilder Cookie
{
get => _cookieBuilder;
set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
}
public IDataProtectionProvider DataProtectionProvider { get; set; }
public bool SlidingExpiration { get; set; }
public PathString LoginPath { get; set; }
public PathString LogoutPath { get; set; }
...
}
While invoking AddCookie
methods in the ConfigureServices
to provide required option to validate:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/LogIn";
options.LogoutPath = "/Account/LogOff";
});
}
At AuthenticationHandler
validate or take action based on the specified parameters.
AuthenticationHandler
will do the magic, it will do actual authentication. AuthenticationHandler
helps to do whatever we want to do based on provided AuthenticationSchemeOptions
. The 'HandleAuthenticateAsync
' method will do everything for Authenticating the request.
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
AuthenticationHandler
will be supported by AuthenticationResult
and AuthenticationTicket
.
AuthenticationResult
- This is a simple class that contains the result of an Authenticate
call.
public class AuthenticateResult
{
...
public AuthenticationTicket Ticket { get; protected set; }
public static AuthenticateResult Success(AuthenticationTicket ticket)
{
if (ticket == null)
{
throw new ArgumentNullException(nameof(ticket));
}
return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
}
public static AuthenticateResult Fail(Exception failure)
{
return new AuthenticateResult() { Failure = failure };
}
public static AuthenticateResult NoResult()
{
return new AuthenticateResult() { None = true };
}
...
}
HandleAuthenticateAsync
will return AuthenticationResult
object - this result will indicate the Authentication Result. This result class is supported by AuthenticationTicket
.
AuthenticationTicket
is the successful result of the AuthenticationResult
object. Without AuthenticationTicket
, the Authentication cannot be successful.
public class AuthenticationTicket
{
public AuthenticationTicket(ClaimsPrincipal principal,
AuthenticationProperties properties, string authenticationScheme)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
AuthenticationScheme = authenticationScheme;
Principal = principal;
Properties = properties ?? new AuthenticationProperties();
}
...
}
AuthenticationHandler
can be derived and validated different parameters based on different requirements, just we look into CookieAuthenticationHandler
:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var result = await EnsureCookieTicket();
if (!result.Succeeded)
{
return result;
}
var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket);
await Events.ValidatePrincipal(context);
if (context.Principal == null)
{
return AuthenticateResult.Fail("No principal.");
}
if (context.ShouldRenew)
{
RequestRefresh(result.Ticket);
}
return AuthenticateResult.Success(new AuthenticationTicket
(context.Principal, context.Properties, Scheme.Name));
}
This handler will call EnsureCookieTicket()
and returns the AuthenticateResult
based on condition check.
private Task<AuthenticateResult> EnsureCookieTicket()
{
if (_readCookieTask == null)
{
_readCookieTask = ReadCookieTicket();
}
return _readCookieTask;
}
private async Task<AuthenticateResult> ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name);
if (string.IsNullOrEmpty(cookie))
{
return AuthenticateResult.NoResult();
}
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
if (ticket == null)
{
return AuthenticateResult.Fail("Unprotect ticket failed");
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null)
{
return AuthenticateResult.Fail("SessionId missing");
}
_sessionKey = claim.Value;
ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
if (ticket == null)
{
return AuthenticateResult.Fail("Identity missing in session store");
}
}
var currentUtc = Clock.UtcNow;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
return AuthenticateResult.Fail("Ticket expired");
}
CheckForRefresh(ticket);
return AuthenticateResult.Success(ticket);
}
In Cookie authentication, every request has to go through CookieAuthenticationHandler
and validated cookie data and AuthenticateResult
will indicate the cookie validation result.
In ASP.NET core, the complete authentication process is pretty simple and easy to provide custom authentication feature.