ASP.NET 5. Token Authentication

I needed to write some 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 it were a regular MVC application, I would use cookie-based authentication, but cross-domain requests do not allow cookies. Therefore, you need to use token-based authentication.

Microsoft offers its implementation - JwtBearerAuthentication. But the hunt itself to figure it out. So I decided to write my implementation - BearerAuthentication.

User Authentication Algorithm


The user enters a username and password, which are sent to the server with a POST request via AJAX. The server authenticates the user and generates a token that sends to the user in the response headers. With each new request for the API, 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, again cookies, but now it is used only in the client part).

Implementation


The current version of ASP.NET 5 is 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. The app.UseMiddleware () method is simply called here;
BearerAuthenticationMiddleware - inherits the AuthenticationMiddleware class;
BearerAuthenticationOptions - inherits the AuthenticationOptions class;
BearerAuthenticationHandler - inherits the AuthenticationHandler class and is the main class for handling authentication requests.
Helper Classes:
BearerAuthenticationDefaults- contains string constants AuthenticationScheme and HeaderName;
IBearerAuthenticationEvents is an interface that defines methods that are called from the BearerAuthenticationHandler to enable the ability to process requests outside of middleware. The implementation of this interface can be specified in BearerAuthenticationOptions.

Consider the BearerAuthenticationOptions class.

public class BearerAuthenticationOptions : AuthenticationOptions, IOptions
{
    public BearerAuthenticationOptions()
    {
        AuthenticationScheme = BearerAuthenticationDefaults.AuthenticationScheme;
        HeaderName = BearerAuthenticationDefaults.HeaderName;
        SystemClock = new SystemClock();
        Events = new BearerAuthenticationEvents();
    }
    public string HeaderName { get; set; }
    public ISecureDataFormat 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 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 in order to check the expiration of the token.

The BearerAuthenticationMiddleware class has an overridden CreateHandler () method 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 have to create a ticket (AuthenticationTicket), encrypt it and write it in the response headers. The ticket is formed from ClaimsPrincipal, AuthenticationProperties and AuthenticationScheme;
HandleSignOutAsync - here we will simply write a void in the response header so that the client application accepts an empty token;
HandleAuthenticateAsync - request handler - here we must decrypt the token from the header into the ticket, and check its expiration date;
HandleUnauthorizedAsync - we will respond to unauthorized requests with a code of 401;
HandleForbiddenAsync - for requests to which the user is denied access, we respond with code 403;
FinishResponseAsync - called after each request handler.

Source code of the class:

public class BearerAuthenticationHandler : AuthenticationHandler
{
    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 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 HandleUnauthorizedAsync(ChallengeContext context)
    {
        Response.StatusCode = 401;
        var unauthorizedContext = new BearerUnauthorizedContext(Context, Options);
        await Options.Events.Unauthorized(unauthorizedContext);
        return true;
    }
    protected override async Task 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 authentication process. Of course, you can still expand this handler by adding, for example, the ability to save a token in a session in ITicketStore.

Project sources can be taken on GitHub .

Also popular now: