Adding Basic Auth to your MVC application in .NET Core

basic-auth

Pros and cons?

So you want to secure your api or you mvc application? And you want to keep it really simple! Well then Basic Auth might be just right for you!

Before you get started with applying basic auth it's good to keep in mind that Basic Auth it comes with some disadvantages that could be considered deal-breaker, but knowing your weakness allows you to take actions and handle them.

Here I've listed som pros and cons for the basic auth protocol.

Pros

Cons

  • The credentials for the user are sent with each request.
  • The credentials are sent as plaintext
  • No way to log out, except by ending the session
  • Could be vulnerable to cross-site request forgery (CSRF)

Solutions

There are a few ways to solve this problem, down below you can find two of them.

Attribute and filter solution

If you want the possibility to specify on a controller and/or method level the right choice for you could be

Example Code

BasicAuthorizeAttribute.se

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace API.BasicAuth.Attributes
{
    using System;
    using Microsoft.AspNetCore.Mvc;

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class BasicAuthorizeAttribute : TypeFilterAttribute
    {
        public BasicAuthorizeAttribute(string realm = null)
            : base(typeof(BasicAuthorizeFilter))
        {
            Arguments = new object[]
            {
                realm
            };
        }
    }
}

BasicAuthorizeFilter.cs

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace API.BasicAuth.Attributes
{
    using System;
    using System.Text;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;

    public class BasicAuthorizeFilter : IAuthorizationFilter
    {
        private readonly string realm;

        public BasicAuthorizeFilter(string realm = null)
        {
            this.realm = realm;
        }

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            string authHeader = context.HttpContext.Request.Headers["Authorization"];
            if (authHeader != null && authHeader.StartsWith("Basic "))
            {
                // Get the encoded username and password
                var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();

                // Decode from Base64 to string
                var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));

                // Split username and password
                var username = decodedUsernamePassword.Split(':', 2)[0];
                var password = decodedUsernamePassword.Split(':', 2)[1];

                // Check if login is correct
                if (IsAuthorized(username, password))
                {
                    return;
                }
            }

            // Return authentication type (causes browser to show login dialog)
            context.HttpContext.Response.Headers["WWW-Authenticate"] = "Basic";

            // Add realm if it is not null
            if (!string.IsNullOrWhiteSpace(realm))
            {
                context.HttpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{realm}\"";
            }

            // Return unauthorized
            context.Result = new UnauthorizedResult();
        }

        // Make your own implementation of this
        public bool IsAuthorized(string username, string password)
        {
            // Check that username and password are correct
            return username.Equals("User1", StringComparison.InvariantCultureIgnoreCase)
                   && password.Equals("SecretPassword!");
        }
    }
}

Example usage

SecuredController.cs

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace API.BasicAuth.Controllers
{
    using System.Collections.Generic;
    using Attributes;
    using Microsoft.AspNetCore.Mvc;

    [BasicAuthorize("my-example-realm.com")]
    [Route("api/[controller]")]
    public class SecuredController : Controller
    {
        // GET api/secured
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new[] { "Secret 1", "Secret 2" };
        }
    }
}

Middleware solution

We have the possibility to write a middleware to secure the site with basic auth. In the sample below we are securing the entire site with basic auth but it could be rewriten (or mapped) to only effect certain endpoints.

Example Code

When created the middleware you can use it by adding app.UseMiddleware<BasicAuthMiddleware>("example-realm.com"); to your Startup.cs.

BasicAuthMiddleware.cs

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace API.BasicAuth.Middlewares
{
    using System;
    using System.Net;
    using System.Text;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Routing;

    public class BasicAuthMiddleware
    {
        private readonly RequestDelegate next;
        private readonly string realm;

        public BasicAuthMiddleware(RequestDelegate next, string realm)
        {
            this.next = next;
            this.realm = realm;
        }

        public async Task Invoke(HttpContext context)
        {
            string authHeader = context.Request.Headers["Authorization"];
            if (authHeader != null && authHeader.StartsWith("Basic "))
            {
                // Get the encoded username and password
                var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();

                // Decode from Base64 to string
                var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));

                // Split username and password
                var username = decodedUsernamePassword.Split(':', 2)[0];
                var password = decodedUsernamePassword.Split(':', 2)[1];

                // Check if login is correct
                if (IsAuthorized(username, password))
                {
                    await next.Invoke(context);
                    return;
                }
            }

            // Return authentication type (causes browser to show login dialog)
            context.Response.Headers["WWW-Authenticate"] = "Basic";

            // Add realm if it is not null
            if (!string.IsNullOrWhiteSpace(realm))
            {
                context.Response.Headers["WWW-Authenticate"] += $" realm=\"{realm}\"";
            }

            // Return unauthorized
            context.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
        }

        // Make your own implementation of this
        public bool IsAuthorized(string username, string password)
        {
            // Check that username and password are correct
            return username.Equals("User1", StringComparison.InvariantCultureIgnoreCase)
                   && password.Equals("SecretPassword!");
        }
    }
}

Final words

Often my solution of choice is the Attribute/Filter solution since I think it's more flexible. But which ever you choose both will work.

You can also find the solutions in a project on github, which you can find at github.com/zarxor/Example.API.Secured.

And remember, always ... ALWAYS (!) use HTTPS.

Share post
About Johan Boström

I'm a system architect, developer and solution expert, with experience in web-, application-, server- and windows service development with main focus on .NET / C#. Special skills with many kinds of CMS-tools, like EPiServer, Umbraco and Litium Studio.

Comments

Related posts

Stop light
Request throttling in .NET Core MVC

Security in apis are important and we might not want the apis we build to be overly used. I built a small attribute function that allows for throttling of a specific endpoint.