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);
What type is Scheme.Name here?