📜 ⬆️ ⬇️

ASP.NET 5. Token authentication

I needed to write a certain ASP.NET WebApi application, and a Javascript client application using this API. It was decided to write on ASP.NET 5, at the same time and study the new release.

If this were a normal MVC application, I would use cookie-based authentication, but cross-domain requests do not allow the transfer of cookies. Therefore, it is necessary to use token-based authentication.

Microsoft offers its implementation - JwtBearerAuthentication. But the hunt is the same to understand everything. So I decided to write my own implementation - BearerAuthentication.

User Authentication Algorithm


The user enters the username and password, which are sent by a POST request via AJAX to the server. The server authenticates the user and generates some kind of token that is sent to the user in the response headers. With each new API request, the client application will have to send the received token in the request header. In order not to lose it, you can store the token in cookies (yes, cookies again, but now it is used only in the client part).
')

Implementation


The current version of ASP.NET 5 - RC1 Update1. To implement authentication, we need the Microsoft.AspNet.Authentication package.
The following is a list of the main classes that need to be implemented:

BearerAuthenticationExtensions - contains the UseBearerAuthentication methods of the IApplicationBuilder interface extension. Here, the app.UseMiddleware () method is simply called;
BearerAuthenticationMiddleware - inherits the AuthenticationMiddleware class;
BearerAuthenticationOptions - inherits the AuthenticationOptions class;
BearerAuthenticationHandler - inherits the AuthenticationHandler class and is the main class for handling authentication requests.
Auxiliary classes:
BearerAuthenticationDefaults - contains the string constants AuthenticationScheme and HeaderName;
IBearerAuthenticationEvents is an interface that defines methods that are called from BearerAuthenticationHandler to enable the ability to process requests outside the middleware. The implementation of this interface can be specified in BearerAuthenticationOptions.

Consider the BearerAuthenticationOptions class.

public class BearerAuthenticationOptions : AuthenticationOptions, IOptions<BearerAuthenticationOptions> { public BearerAuthenticationOptions() { AuthenticationScheme = BearerAuthenticationDefaults.AuthenticationScheme; HeaderName = BearerAuthenticationDefaults.HeaderName; SystemClock = new SystemClock(); Events = new BearerAuthenticationEvents(); } public string HeaderName { get; set; } public ISecureDataFormat<AuthenticationTicket> TicketDataFormat { get; set; } public IDataProtectionProvider DataProtectionProvider { get; set; } public ISystemClock SystemClock { get; set; } public IBearerAuthenticationEvents Events { get; set; } public BearerAuthenticationOptions Value => this; } 

TicketDataFormat will be used to encrypt and decrypt the token. If the TicketDataFormat is not passed in the parameters, then it will be generated based on the passed DataProtectionProvider. SystemClock is needed to get the current date to check the expiration date of the token.

The BearerAuthenticationMiddleware class has an override method CreateHandler () that returns a new instance of the BearerAuthenticationHandler class.

Now let's look at how authentication requests are processed in the BearerAuthenticationHandler class. This class contains several overridden methods:

HandleSignInAsync - here we need to create a ticket (AuthenticationTicket), encrypt it and write to the response headers. Ticket is formed from ClaimsPrincipal, AuthenticationProperties and AuthenticationScheme;
HandleSignOutAsync - here we will simply write an empty message in the response header so that the client application accepts an empty token;
HandleAuthenticateAsync - request handler - here we have to decrypt the token from the header to the ticket, and check its expiration date;
HandleUnauthorizedAsync - we will respond to unauthorized requests with code 401;
HandleForbiddenAsync - to requests that the user is denied access, respond with code 403;
FinishResponseAsync - called after each request handler.

Class source code:

 public class BearerAuthenticationHandler : AuthenticationHandler<BearerAuthenticationOptions> { private bool _shouldRenew; private AuthenticationTicket GetTicket() { if (!Context.Request.Headers.ContainsKey(Options.HeaderName)) return null; var bearer = Context.Request.Headers[Options.HeaderName]; if (string.IsNullOrEmpty(bearer)) return null; var ticket = Options.TicketDataFormat.Unprotect(bearer); if (ticket == null) return null; var currentUtc = Options.SystemClock.UtcNow; var expiresUtc = ticket.Properties.ExpiresUtc; if (expiresUtc.HasValue && expiresUtc.Value < currentUtc) return null; return ticket; } private void ApplyBearer(AuthenticationTicket ticket) { if (ticket != null) { var protectedData = Options.TicketDataFormat.Protect(ticket); Response.Headers["Access-Control-Expose-Headers"] = Options.HeaderName; Response.Headers[Options.HeaderName] = protectedData; } else { Response.Headers["Access-Control-Expose-Headers"] = Options.HeaderName; Response.Headers[Options.HeaderName] = StringValues.Empty; } } protected override async Task HandleSignInAsync(SignInContext signIn) { var signingInContext = new BearerSigningInContext(Context, Options, signIn.Principal, new AuthenticationProperties(signIn.Properties)); await Options.Events.SigningIn(signingInContext); var ticket = new AuthenticationTicket(signingInContext.Principal, signingInContext.Properties, Options.AuthenticationScheme); ApplyBearer(ticket); var signedInContext = new BearerSignedInContext(Context, Options, signingInContext.Principal, signingInContext.Properties); await Options.Events.SignedIn(signedInContext); } protected override async Task HandleSignOutAsync(SignOutContext context) { var signingOutContext = new BearerSigningOutContext(Context, Options); await Options.Events.SigningOut(signingOutContext); ApplyBearer(null); } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { var ticket = GetTicket(); if (ticket == null) return AuthenticateResult.Failed("No ticket."); var context = new BearerValidatePrincipalContext(Context, Options, ticket.Principal, ticket.Properties); await Options.Events.ValidatePrincipal(context); if (context.Principal == null) return AuthenticateResult.Failed("No principal."); if (context.ShouldRenew) _shouldRenew = true; return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme)); } protected override async Task<bool> HandleUnauthorizedAsync(ChallengeContext context) { Response.StatusCode = 401; var unauthorizedContext = new BearerUnauthorizedContext(Context, Options); await Options.Events.Unauthorized(unauthorizedContext); return true; } protected override async Task<bool> HandleForbiddenAsync(ChallengeContext context) { Response.StatusCode = 403; var forbiddenContext = new BearerForbiddenContext(Context, Options); await Options.Events.Forbidden(forbiddenContext); return true; } protected override async Task FinishResponseAsync() { if (!_shouldRenew || SignInAccepted || SignOutAccepted) return; var result = await HandleAuthenticateOnceAsync(); var ticket = result?.Ticket; if (ticket == null) return; ApplyBearer(ticket); } } 

User authentication can be called, for example, from the controller:

 await HttpContext.Authentication.SignInAsync(BearerAuthenticationDefaults.AuthenticationScheme, principal); 

where principal is an instance of the ClaimsPrincipal class that will be passed to the HandleSignInAsync method.

This example only demonstrates the processing of authentication. Of course, you can still extend this handler by adding, for example, the ability to save a token in a session in the ITicketStore.

Project sources can be downloaded from GitHub .

Source: https://habr.com/ru/post/274163/


All Articles