Identity is the main package for managing authentication and authorization in an ASP.NET Core application. This package allows user registration and login, and uses claims to control user access within the application.
A new ASP.NET Core application can be initialized with Identity through the Visual Studio project creation wizard. This process provides additional UI elements to the basic Razor Pages layout including Login and Registration forms, and corresponding navigation buttons in the header.
The following examples show how Identity can be configured in an ASP.NET Core application to provide user authentication, user model integration, app-wide and route-specific authorization, and claim-based view controls.
Configuring the service
The options for configuring the Identity service can be provided when registering the service in Program.cs. Options include password requirements, account confirmation, lockout policies, and other constraints.
builder.Services
.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ApplicationDbContext>();
The out-of-the-box implementation of Identity uses a IdentityUser base class as the default identity class when configuring the service. It is important to create a ApplicationUser class that extends this class to allow for application-specific customization.
public class ApplicationUser : IdentityUser
{
public virtual ICollection<Ticket> ReportedTickets { get; set; } = new List<Ticket>();
public virtual ICollection<Ticket> AssignedTickets { get; set; } = new List<Ticket>();
}
Relating models to users
Other application models can use the ApplicationUser class to form relationships with users and navigate to them through the initial object.
public class Ticket
{
[Key]
public int TicketId { get; set; }
public string? AssigneeId { get; set; }
public ApplicationUser? Assignee { get; set; }
public string? ReporterId { get; set; }
public ApplicationUser? Reporter { get; set; }
}
To be able to access related ApplicationUser objects through an inital object the repository needs to include these additional objects when requesting the initial object from the database. This approach is standard in Entity Framework when loading related models.
public async Task<Ticket?> GetByIdAsync(int id)
{
var ticket = await _context.Tickets
.Include(t => t.Assignee)
.Include(t => t.Reporter)
.FirstOrDefaultAsync(m => m.TicketId == id);
return ticket;
}
Protecting the application
The entire application can be protected by chaining the RequireAuthorization method onto the MapRazorPages method in Program.cs. This will then force users to log in to access the rest of the application.
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages().RequireAuthorization();
Authorization policies can also be registered in Program.cs, and rules for these are based on key-value pairs called Claims.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanViewAllTickets", policy => policy.RequireClaim("IsAdmin", "true"));
options.AddPolicy("CanDeleteTicket", policy => policy.RequireClaim("IsAdmin", "true"));
options.AddPolicy("CanEditTicket", policy => policy.RequireClaim("IsAdmin", "true"));
});
Claims can be assigned to users through business logic within the application, but they can also be added using the terminal for quick and easy testing, or to elevate an initial admin user.
> sqlite3 localdatabase.db
> INSERT INTO AspNetUserClaims (UserId, ClaimType, ClaimValue) VALUES ('###', 'IsAdmin', 'true');
Using policies
A policy can be used to protect an entire set of routes defined in a single page model by using the [Authorize] annotation and passing in the name of the policy. Only users that satisfy the policy (i.e. have the required claims in this case) will then be able to interact with this page. All other users will be met with an access denied message.
[Authorize("CanEditTicket")]
public class EditModel : PageModel
{
//
}
Policies can also be checked within business logic to determine a user’s access. Passing the User object (the Claims Principal provided by the PageModel base class which representes the currently logged in user), along with a policy name to the AuthorizeAsync method of an instantiated UserManager will return an object which can be resolved to determine whether the user satisfies the policy.
public async Task<IActionResult> OnGetAsync()
{
var canViewAllTicketsCheck = await _authService.AuthorizeAsync(User, "CanViewAllTickets");
var canViewAllTickets = canViewAllTicketsCheck.Succeeded;
if (canViewAllTickets)
{
Tickets = await _ticketService.GetAllTicketsAsync();
}
else
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
Tickets = await _ticketService.GetTicketsByReporterIdAsync(userId!);
}
return Page();
}
This process of checking whether a user satisfies a policy within business logic can then be used to set a property of the page model, which in turn can be accessed in the view.
public bool CanEditTicket { get; set; } = false;
public bool CanDeleteTicket { get; set; } = false;
public async Task<IActionResult> OnGetAsync()
{
Ticket = await _ticketService.GetTicketByIdAsync(TicketId);
if (Ticket == null)
{
return NotFound();
}
var canEditTicketCheck = await _authService.AuthorizeAsync(User, "CanEditTicket");
var canDeleteTicketCheck = await _authService.AuthorizeAsync(User, "CanDeleteTicket");
CanEditTicket = canEditTicketCheck.Succeeded;
CanDeleteTicket = canDeleteTicketCheck.Succeeded;
return Page();
}
Certain elements of the page can be shown or hidden based on the value of properties which were set according to the user’s access level.
<div class="card-footer text-right">
@if (Model.CanDeleteTicket)
{
<a asp-page="./Delete" asp-route-TicketId="@Model.TicketId" class="btn btn-danger">Delete Ticket</a>
}
@if (Model.CanEditTicket)
{
<a asp-page="./Edit" asp-route-TicketId="@Model.TicketId" class="btn btn-secondary ml-2">Edit Ticket</a>
}
<a asp-page="./Index" class="btn btn-primary ml-2">Back to List</a>
</div>
🛈 The full code for this ASP.NET Core Identity app, including the test suite, can be found on my GitHub.