Authentication and authorization in ASP.NET Core

This tutorial shows you how to implement scheme based autentication and authorization in ASP.NET Core. ASP.NET Core 3.0 is very strict about how authentication and authorization should be implemented.

Scheme based authentication and authorization is very smooth to use, you can use built-in schemes and create your own authentication handlers. You can also add multiple schemes and protect different parts of your website with different schemes. You can also use multiple schemes for a controller.

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.

I am going to implement cookie based authentication, basic authentication and role based authorization in this tutorial.

Configure services

Authentication is added in the ConfigureServices method in the StartUp class. Middlewares for authentication and authorization is added in the Configure method in the StartUp class.

public void ConfigureServices(IServiceCollection services)
{
    // Add the mvc framework
    services.AddRazorPages();

    // Add memory cache
    services.AddDistributedMemoryCache();

    // Add redis distributed cache
    if (configuration.GetSection("AppSettings")["RedisConnectionString"] != "")
    {
        services.AddDistributedRedisCache(options =>
        {
            options.Configuration = configuration.GetSection("AppSettings")["RedisConnectionString"];
            options.InstanceName = "Fotbollstabeller:";
        });
    }

    // Add the session service
    services.AddSession(options =>
    {
        // Set session options
        options.IdleTimeout = TimeSpan.FromMinutes(20d);
        options.Cookie.Name = ".Fotbollstabeller";
        options.Cookie.Path = "/";
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
    });

    // Create database options
    services.Configure<DatabaseOptions>(options => 
    {
        options.connection_string = configuration.GetSection("AppSettings")["ConnectionString"];
        options.sql_retry_count = 1;
    });

    // Create cache options
    services.Configure<CacheOptions>(options => 
    {
        options.expiration_in_minutes = 240d;
    });
            
    // Add Authentication
    services.AddAuthentication()
        .AddCookie("Administrator", options =>
        {
            options.ExpireTimeSpan = TimeSpan.FromDays(10);
            options.Cookie.MaxAge = TimeSpan.FromDays(10);
            options.Cookie.HttpOnly = true;
            options.Cookie.SameSite = SameSiteMode.Lax;
            options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
            options.Events.OnRedirectToLogin = (context) =>
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                context.Response.Redirect("/admin_login");
                return Task.CompletedTask;
            };
        })
        .AddCookie("Member", options => 
        {
            options.ExpireTimeSpan = TimeSpan.FromHours(4);
            options.Cookie.MaxAge = TimeSpan.FromHours(4);
            options.Cookie.HttpOnly = true;
            options.Cookie.SameSite = SameSiteMode.Lax;
            options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
            options.Events.OnRedirectToLogin = (context) =>
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                context.Response.Redirect("/member_login");
                return Task.CompletedTask;
            };
        })
        .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("ApiAuthentication", null);

    // Add clients
    services.AddHttpClient();

    // Add repositories
    services.AddSingleton<IDatabaseRepository, MsSqlRepository>();
    services.AddSingleton<IWebsiteSettingRepository, WebsiteSettingRepository>();
    services.AddSingleton<IAdministratorRepository, AdministratorRepository>();
    services.AddSingleton<IFinalRepository, FinalRepository>();
    services.AddSingleton<IGroupRepository, GroupRepository>();
    services.AddSingleton<IStaticPageRepository, StaticPageRepository>();
    services.AddSingleton<IXslTemplateRepository, XslTemplateRepository>();
    services.AddSingleton<ISitemapRepository, SitemapRepository>();
    services.AddSingleton<IXslProcessorRepository, XslProcessorRepository>();
    services.AddSingleton<ICommonServices, CommonServices>();

} // End of the ConfigureServices method

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Use redirection
    app.UseMiddleware<RedirectMiddleware>();

    // Use error handling
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseStatusCodePagesWithReExecute("/home/error/{0}");
    }

    // Use static files
    app.UseStaticFiles(new StaticFileOptions
    {
        OnPrepareResponse = ctx =>
        {
            // Cache static files for 30 days
            ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=25920000");
            ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(300).ToString("R", CultureInfo.InvariantCulture));
        }
    });

    // Use sessions
    app.UseSession();

    // For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must 
    // appear between the calls to UseRouting and UseEndpoints to be effective.
    app.UseRouting();

    // Use authentication and authorization middlewares
    app.UseAuthentication();
    app.UseAuthorization();

    // Routing endpoints
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            "default",
            "{controller=home}/{action=index}/{id?}");
    });

} // End of the Configure method

Basic Authentication

I have created a custom handler for basic authentication. I am using this handler in the ApiAuthentication scheme.

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    #region Variables

    private readonly IAdministratorRepository administrator_repository;

    #endregion

    #region Constructors

    public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger,
        UrlEncoder encoder, ISystemClock clock, IAdministratorRepository administrator_repository)
        : base(options, logger, encoder, clock)
    {
        // Set instance variables
        this.administrator_repository = administrator_repository;

    } // End of the constructor

    #endregion

    #region Methods

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Make sure that there is an Authorization header
        if (Request.Headers.ContainsKey("Authorization") == false)
        {
            // Return failure
            return AuthenticateResult.Fail("No Authorization header");
        }

        // Get the authorization header
        string authHeader = Request.Headers["Authorization"];

        // 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 username = decodedToken.Substring(0, seperatorIndex);
        string password = decodedToken.Substring(seperatorIndex + 1);

        // Get a api user, username must be unique
        Administrator api_user = await this.administrator_repository.GetApiUser(username, password);

        // Make sure that the username and password is correct
        if(api_user != null)
        {
            // Create claims
            ClaimsIdentity identity = new ClaimsIdentity(Scheme.Name);
            identity.AddClaim(new Claim("user", JsonConvert.SerializeObject(api_user)));
            ClaimsPrincipal principal = new ClaimsPrincipal(identity);
            AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);

            // Return success
            return AuthenticateResult.Success(ticket);
        }
        else
        {
            // Return failure
            return AuthenticateResult.Fail("Incorrect username or password");
        }

    } // End of the HandleAuthenticateAsync method

    #endregion

} // End of the class

Require authentication and authorization

Add a authorize attribute to controllers and/or methods that requires authentication, you can specify one or more schemes that should be used. The default schemes is used if no scheme is specified. Add roles if you want to restrict access to users with certain roles.

// One scheme
[Authorize(AuthenticationSchemes = "Administrator")]
public class admin_xsl_templatesController : Controller

// Multiple schemes
[Authorize(AuthenticationSchemes = "Administrator,Member")]
public class admin_xsl_templatesController : Controller

// Protect a method by only allowing some roles
[HttpGet]
[Authorize(Roles = "Administrator,Editor")]
public async Task<IActionResult> index()

// Api authentication
[Route("api/jobs/[action]")]
[Authorize(AuthenticationSchemes = "ApiAuthentication")]
public class JobsController : Controller

Log in and log out

I have log in form on my website that calls the login method. I add claims and logs in the user with the “Administrator” scheme. Don’t save to much information in claims for cookie-based authentication, claims is saved in the cookie. Save an identifier and use that identifier to get a user.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> login(IFormCollection collection)
{
    // Get the data from the form
    string username = collection["txtUsername"];
    string password = collection["txtPassword"];

    // Get the administrator
    Administrator administrator = this.administrator_repository.GetOneByUsername(username);

    // Create response data
    ResponseData data = null;

    // Check if the user name exists and if the password is correct
    if (administrator != null && this.administrator_repository.ValidatePassword(administrator.id, password) == true)
    {
        // Create claims
        ClaimsIdentity identity = new ClaimsIdentity("Administrator");
        //identity.AddClaim(new Claim("administrator", JsonConvert.SerializeObject(administrator)));
        identity.AddClaim(new Claim(ClaimTypes.Name, administrator.admin_user_name));
        identity.AddClaim(new Claim(ClaimTypes.Role, administrator.admin_role));
        ClaimsPrincipal principal = new ClaimsPrincipal(identity);

        // Sign in the administrator
        await HttpContext.SignInAsync("Administrator", principal);

        // Add success data
        data = new ResponseData(true, username, $"Du har nu loggats in!");
    }
    else
    {
        // Add error data
        data = new ResponseData(false, username, $"Användarnamnet eller lösenordet är felaktigt!");
    }

    // Return the data
    return Json(data: data);

} // End of the post login method

[HttpGet]
public async Task<IActionResult> logout()
{
    // Sign out the administrator
    await HttpContext.SignOutAsync("Administrator");

    // Redirect the user to the login page
    return RedirectToAction("index", "admin_login");

} // End of the logout method

Get user information

We can easily get information about a signed in user in any of our controller methods from HttpContext as we saved this information as claims.

// Get Json document
Claim claim = HttpContext.User.FindFirst("administrator");
Administrator user = JsonConvert.DeserializeObject<Administrator>(claim.Value);

// Get administrator from username
Administrator user = this.administrator_repository.GetOneByUsername(HttpContext.User.Identity.Name);

Leave a Reply

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