Authentication and authorization in ASP.NET Core

This post describes how you can add authentication and authorization to your website in ASP.NET Core. We are going to use a middleware to protect some parts of our website by requiring a user to be signed in with a username and a password, some methods will also be restricted to users with certain roles (authorization).

Authentication is a process to confirm an identity of a user, a login form is usually used on websites to match a user and a session. Authorization is a process to give access to resources depending on user privileges.

Middleware

We use a middleware to handle authentication and authorization for different parts of the website. We have an api that can be accessed by members, an administrator area that can be accessed by administrators and a member area that can be accessed by members. Information about a user is saved as ClaimsPrincipal claimsPrincipal in the user context, this makes it easy to specify an authorize attribute for methods and to get information about the signed in user in methods. Information about a signed in user is also added on parts of the website that is public, this information might be useful to customize contents.

public class AuthorizationMiddleware
{
    #region Variables

    private readonly RequestDelegate next;

    #endregion

    #region Constructors

    public AuthorizationMiddleware(RequestDelegate next)
    {
        // Set values for instance variables
        this.next = next;

    } // End of the constructor

    #endregion

    #region Methods

    public async Task Invoke(HttpContext context, IAdministratorRepository administrator_repository, IMemberRepository member_repository)
    {
        // Get the url path
        string path = context.Request.Path;

        // Respond to different paths
        if (path.StartsWith("/api") == true)
        {
            // Get the authorization header
            string authHeader = context.Request.Headers["Authorization"];

            // Make sure that there is a authorization header
            if (authHeader != null && authHeader.StartsWith("Basic"))
            {
                // Get tokens
                string authToken = authHeader.Substring("Basic ".Length).Trim();
                string decodedToken = Encoding.UTF8.GetString(Convert.FromBase64String(authToken));

                // Get the separator index
                Int32 seperatorIndex = decodedToken.IndexOf(":");

                // Get the username and password
                string email = decodedToken.Substring(0, seperatorIndex);
                string password = decodedToken.Substring(seperatorIndex + 1);

                // Get a member, username must be unique
                ModelItem<MemberDocument> api_user_model = await member_repository.GetApiUser(email, password);

                // Check for correct credentials
                if (api_user_model.item != null)
                {
                    // Create claims
                    ClaimsIdentity claimsIdentity = new ClaimsIdentity("ApiUser");
                    claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, api_user_model.item.id.ToString())); // Important for Antiforgery Ajax post
                    claimsIdentity.AddClaim(new Claim("member", JsonConvert.SerializeObject(api_user_model.item)));
                    //claimsIdentity.AddClaim(new Claim("etag", api_user_model.etag));
                    ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                    context.User = claimsPrincipal;
                }
                else
                {
                    // Unauthorized 401
                    byte[] unauthorized = Encoding.UTF8.GetBytes("Not authorized (401)");
                    context.Response.StatusCode = 401;
                    context.Response.Body.Write(unauthorized, 0, unauthorized.Length);
                    var statusCodePagesFeature = context.Features.Get<IStatusCodePagesFeature>();
                    if (statusCodePagesFeature != null)
                    {
                        statusCodePagesFeature.Enabled = false;
                    }
                    return;
                }
            }
            else
            {
                // Unauthorized 401
                byte[] unauthorized = Encoding.UTF8.GetBytes("Not authorized (401)");
                context.Response.StatusCode = 401;
                context.Response.Body.Write(unauthorized, 0, unauthorized.Length);
                var statusCodePagesFeature = context.Features.Get<IStatusCodePagesFeature>();
                if (statusCodePagesFeature != null)
                {
                    statusCodePagesFeature.Enabled = false;
                }
                return;
            }
        }
        else if (path.StartsWith("/member") == true)
        {
            // Get the signed in member
            ModelItem<MemberDocument> member_model = await member_repository.GetSignedInMember(context);

            if (member_model.item == null)
            {
                // Redirect the user to the log in page
                context.Response.Redirect("/auth/log_in");
                return;
            }
            else if(member_model.item.verified == 0)
            {
                // Redirect the user to the verify email page
                context.Response.Redirect("/auth/verify_email");
                return;
            }
            else
            {
                // Create claims
                ClaimsIdentity claimsIdentity = new ClaimsIdentity("User");
                claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, member_model.item.id.ToString())); // Important for Antiforgery Ajax post
                claimsIdentity.AddClaim(new Claim("member", JsonConvert.SerializeObject(member_model.item)));
                ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                context.User = claimsPrincipal;
            }
        }
        else if (path.StartsWith("/admin_") == true && path.StartsWith("/admin_login") == false)
        {
            // Get the signed in administrator
            ModelItem<AdministratorDocument> administrator_model = await administrator_repository.GetSignedInAdministrator(context);

            if (administrator_model.item == null)
            {
                // Redirect the user to the login page
                context.Response.Redirect("/admin_login");
                return;
            }
            else
            {
                // Create claims
                ClaimsIdentity claimsIdentity = new ClaimsIdentity("Administrator");
                claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, administrator_model.item.id.ToString())); // Important for Antiforgery Ajax post
                claimsIdentity.AddClaim(new Claim("administrator", JsonConvert.SerializeObject(administrator_model.item)));
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, administrator_model.item.admin_role));
                ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                context.User = claimsPrincipal;
            }
        }
        else
        {
            // Get the signed in member
            ModelItem<MemberDocument> member_model = await member_repository.GetSignedInMember(context);

            if(member_model.item != null)
            {
                // Create claims
                ClaimsIdentity claimsIdentity = new ClaimsIdentity("User");
                claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, member_model.item.id.ToString())); // Important for Antiforgery Ajax post
                claimsIdentity.AddClaim(new Claim("member", JsonConvert.SerializeObject(member_model.item)));
                ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                context.User = claimsPrincipal;
            }   
        }

        // Call the next delegate/middleware in the pipeline
        await next(context);

    } // End of the Invoke method

    #endregion

} // End of the class

Get signed in member

We have a method to get a signed in member that is used in our middleware, this method gets the signed in member and increases the expiration date of the cookie to get a sliding expiration.

public async Task<ModelItem<MemberDocument>> GetSignedInMember(HttpContext context)
{
    // Create the model to return
    ModelItem<MemberDocument> model = new ModelItem<MemberDocument>();

    // Get the cookie
    string cookie = context.Request.Cookies["Member"];

    if (cookie != null)
    {
        model = await GetById(this.data_protector.Unprotect(cookie));

        // Renew the cookie
        CookieOptions options = new CookieOptions();
        options.Expires = DateTime.UtcNow.AddDays(1);
        options.HttpOnly = true;
        options.SameSite = SameSiteMode.Lax;
        context.Response.Cookies.Append("Member", cookie, options);
    }

    // Return the model
    return model;

} // End of the GetSignedInMember method

Services

We need to tell ASP.NET Core to use our middleware and we add the following contents to the Configure method in the StartUp class.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Use authorization middleware
    app.UseMiddleware<AuthorizationMiddleware>();

} // End of the Configure method

Log in and log out

We have a form to log in members and we use cookies to store information about members that is signed in. The log in form is posted to the log_in method in the auth controller. We validate the password entered by the person how wants to log in, passwords is hashed in the database.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> log_in(IFormCollection collection)
{
    // Get the current web domain
    WebDomain current_domain = await this.web_domain_repository.GetCurrentDomain(ControllerContext.HttpContext);

    // Get translated texts
    KeyStringList tt = await this.static_text_repository.GetFromCache(current_domain.front_end_language_code);

    // Get the email and the password
    string email = collection["txtEmail"].ToString();
    string password = collection["txtPassword"].ToString();

    // Get the member
    ModelItem<MemberDocument> member_model = await this.member_repository.GetByEmail(email);
    MemberDocument post = member_model.item;

    // Check if the member exists and if the password is correct
    if (post != null && await this.member_repository.ValidatePassword(post, password) == true)
    {
        // Create a member cookie
        CookieOptions options = new CookieOptions();
        options.Expires = DateTime.UtcNow.AddDays(1);
        options.HttpOnly = true;
        options.SameSite = SameSiteMode.Lax;
        ControllerContext.HttpContext.Response.Cookies.Append("Member", this.data_protector.Protect(post.id.ToString()), options);

        // Return success data
        return Json(data: new ResponseData(true, "/member", ""));
    }
    else
    {
        // Return error data
        return Json(data: new ResponseData(false, "", tt.Get("error_log_in")));
    }

} // End of the log_in method

To log out a user we only need to change the expiration date of the cookie to a date before the current date and time.

[HttpGet]
public IActionResult log_out()
{
    // Delete the member cookie
    CookieOptions options = new CookieOptions();
    options.Expires = DateTime.UtcNow.AddDays(-1);
    options.HttpOnly = true;
    ControllerContext.HttpContext.Response.Cookies.Append("Member", "", options);

    // Redirect the user to the login page
    return Redirect("/auth/log_in");

} // End of the log_out method

Get user context

We can easily get information about a signed in user in any of our controllers from http context as we saved this information in our authorization middleware.

// Get a member
Claim claim = ControllerContext.HttpContext.User.FindFirst("member");
MemberDocument member = JsonConvert.DeserializeObject<MemberDocument>(claim.Value);

Authorization attribute

We can add an authorization attribute to controllers or methods in our project to restrict access to certain roles. We added a role claim to a administrator in our middleware and this role is matched against the roles added to the attribute. An administrator needs one of the roles stated in the attribute to be able to access a controller or a method. A user needs a role of Administrator or Editor to be able to access the method below.

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Administrator,Editor")]
public async Task<IActionResult> index(IFormCollection collection)
{
    // Code ...

} // End of the index method

Leave a Reply

Your email address will not be published. Required fields are marked *