(C# ASP.NET Core) Adding a Login Page to an Identity based Authentication Project

This tutorial shall continue from the previous tutorial where we have made a basic initialization, and we have also created a database to hold the data of our users. In this tutorial we shall make additional configuration to specify our login and access denied pages. We shall also add a restricted (or protected page) and verify that when an un-authenticated user tries to access it, then he is automatically redirected to the login page.
(Rev. 19-Mar-2024)

Categories | About |     |  

Parveen,

Table of Contents (top down ↓)

Configure the Program.cs File

Let us open the project from the point at which we left it at the end of the previous tutorial.

Open the program.cs file to add and configure services for Identity and Authentication Cookie.

Recall that we have already added a service for DbContext. Just next to it, add the services for identity. This can be done with a single call to AddDefaultIdentity, and AddEntityFrameworkStores.

// Visual Studio 2022 and later, 
// .NET 6 and later 
// Program.cs file 
using Microsoft.AspNetCore.Authentication.Cookies;

using Microsoft.AspNetCore.Identity;

using MyRazorApp.Data;

var builder = WebApplication.CreateBuilder();

builder.Services.AddDbContext<MyAuthContext>();


// add services for identity 
builder.Services
.AddDefaultIdentity<IdentityUser>(
  options => options.SignIn.RequireConfirmedAccount = true
)
.AddEntityFrameworkStores<MyAuthContext>();


// authentication 
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{

  options.LoginPath = "/Auth/Login";

  options.AccessDeniedPath = "/Auth/AccessDenied";

  // Cookie settings 
  // prevent cookie from being accessed 
  // through javascript on the client side 
  options.Cookie.HttpOnly = true;

  options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

  options.SlidingExpiration = true;

}

);

builder.Services.AddRazorPages();

var app = builder.Build();

// order is important 
app.UseAuthentication();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Next add a service for authentication, and also add various options for the authentication cookie. We can set the path to the login page; and to the access denied page (we shall later add both these pages at these paths). We can even set other cookie settings, like the expiry timespan.

We must also add authentication and authorization middleware to the request pipeline. They can be added with the UseAuthentication and UseAuthorization functions. The order of these calls is important - authentication should precede authorization. The sequence of these calls must also be before the MapRazorPages call.

Video Explanation (see it happen!)

Please watch the following youtube video:

Add a Home Page and a Restricted Page

Now let us add an index home page for our website. This will be a public page accessible to everyone - whether authenticated or not.

For this, right click the Pages folder, and add a page called index.cshtml. We have written some random html in it, and also added a link to our restricted page.

// index.cshtml 
// home page of our website 
@page

<h1>Welcome to Identity</h1>

<p>

  IMPORTANT: Please run migration commands as
  explained in the first tutorial. 
  Otherwise things will not work. 
  Migration commands will create database on YOUR PC.

</p>

<a href="/Restricted">Click to View Restricted Page</a>

Next add a page called Restricted. We have written some h1 tag in this page, so that we know that we are on this page.

// restricted.cshtml 

@page

@model MyRazorApp.Pages.RestrictedModel

<h1>Only Logged in Users can Reach here</h1>

But the most important part is the backing class of this page. As you can see, we have added an Authorize attribute to this class. This will restrict access only to authorized users.

// restricted.cshtml.cs 
using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyRazorApp.Pages
{

  [Authorize]
  public class RestrictedModel : PageModel
  {

  }

}

Let us run the project so far. [see the linked video]

The index home page opens; it shows some html and a link to the restricted page; let us click this link. And, as expected, the page doesn't open, and we get an http 404 error message that the Login page has not been found. This has happened as expected because the user was redirected to the login page that hasn't been added yet.

Add the Access Denied and Login Pages

Now it is time to add the access denied and login pages.

It is usually better to keep these pages in a separate Area. It makes the code readable, neat and maintenable as well.

So Add an Area called Auth (see video for screenshot). If you are new to the concept of areas, then we suggest that you may please refer the chapter on "Areas" for more information on this concept.

For now, let us add our AccessDenied and Login pages to the Auth area. This will be the same path as we have set earlier in the Program.cs file.

Right click the Pages folder and add a razor page called AccessDenied. Write any html into it so that we can know that we are on the access denied page.

// access denied page under Area -> Auth -> Pages 
// accedd 
// AccessDenied.cshtml 
@page

<h1>Access Denied</h1>

Again, right click the Pages folder and add a page called Login. This page will present a login form to the user. Let us open the page and examine the form that we have written here.


// login page under Area -> Auth -> Pages 
// Login.cshtml 

@page

@model MyRazorApp.Areas.Auth.Pages.LoginModel

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Please login</h1>

<div style="color:red" asp-validation-summary="All"></div>

<form method="post">

  <table>

  <tr>
    <td>
      <label asp-for="UserID"></label>
    </td>
    
    <td>
      <input asp-for="UserID" />
    </td>

    <td>
      <span asp-validation-for="UserID"></span>
    </td>

  </tr>

  <tr>
      <td>
        <label asp-for="UserPassword"></label>
      </td>

      <td>
        <input asp-for="UserPassword" />
      </td>

      <td>
        <span asp-validation-for="UserPassword"></span>
      </td>

  </tr>

  <tr>
    <td>
    </td>

    <td>

      <input asp-for="RemeberMe" />

      <label asp-for="RemeberMe"></label>

    </td>

    <td>
      <span asp-validation-for="RemeberMe"></span>
    </td>
  </tr>

  <tr>

    <td colspan="2" style="text-align:right">

      <input type="submit" value="login!" />

    </td>

  </tr>

  </table>

</form>

<a asp-page="Register">Register</a> 
|
<a asp-page="ForgotPassword">Forgot Password</a> 
|
<a asp-page="ResendEMailConfirmation">Resend EMail Confirmation</a>


    

First we have added three directives for page, model and addtaghelper. Then we have a validation summary tag for showing all the validation errors at near the top of this page.

A table has been used to create the form. The first tr row is for the user id. It is bound to a property on the backing class. The next tr is for the password. After that we have a remember me checkbox.

Then there is the submit button, and links for the Register, Forgot Password, and Resend Confirmation. We should have used a partial page for these links. But we wanted to keep the things simple for tutorial purposes, so we have added the links here itself. The pages for these links will be added in later tutorials.

Code for the Login Process

Come to the solution explorer and open the Login.cshtml.cs file that contains the backing LoginModel backing class.

Let us examine the various parts of this file.

// Login.cshtml.cs 
using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Mvc.RazorPages;

using System.ComponentModel.DataAnnotations;

namespace MyRazorApp.Areas.Auth.Pages
{

  [AllowAnonymous]
  public class LoginModel : PageModel
  {

    // user id 
    [BindProperty]
    [Required]
    [DataType(DataType.EmailAddress)]
    public String? UserID { get; set; }

    // password 
    [BindProperty]
    [Required]
    [DataType(DataType.Password)]
    public String? UserPassword { get; set; }

    // remember me 
    [BindProperty]
    public Boolean RemeberMe { get; set; }

    // signin service 
    private readonly SignInManager<IdentityUser> _sm;

    public LoginModel(SignInManager<IdentityUser> sm)
    {

      _sm = sm;

    }

    public async Task<IActionResult> OnPostAsync(string? returnUrl)
    {

      if (ModelState.IsValid)
      {

        var result = await _sm.PasswordSignInAsync(
          UserID, UserPassword, RemeberMe, lockoutOnFailure: true );

        if (result.Succeeded)
        {

          // returnUrl is null if the user came 
          // directly to the login page 
          return LocalRedirect(returnUrl ?? "~/");

        }

        else if (result.RequiresTwoFactor)
        {

          // to implement later 
          throw new NotImplementedException();

        }

        else if (result.IsLockedOut)
        {

          // to implement later 
          throw new NotImplementedException();

        }

        else 
        {

          // add to validation summary 
          ModelState.AddModelError(string.Empty, "Login failed. Retry");

        }

      }

      return Page();

    }

  }

}

First of all we have the namespaces.

Then we have the LoginModel class. The class is marked as AllowAnonymous so that the form is publically available.

Then we have 2-way bind pproperties for user id, password and remember me. These properties will bring the form data for the various input elements on the login form.

SignInManager is an identity service that helps us complete the login process. It obtained through a constructor based dependency injection. We have cached it as a readonly property.

The OnPost method executes when the user submits and posts the form. The PasswordSignInAsync method of the SignInManager is used for validating the user id and password combination entered by the user. Notice that all database communication is handled by this single function.

Then there is an if-else ladder for handling the various result outcomes.

If the login succeeds the user is redirected to the return url which contains the page that the user initially wanted to see. But if the user came directly to the login form, then the return url is null, and in that case the user will be redirected to the website home page.

Then we have branches for the case of 2-factor authentication and for the locked out case. These will be implemented in later tutorials.

The login failed message is added to model state in the last else block.

Run the Project so far

Run the project to open the index home page.

Click on the link to attempt a visit to the restricted page. As expected, we will be taken to the login page. This is the page that we configured in the cookie options in the program.cs file.

We haven't yet registered a user, so we can add any random email and password. Click submit to verify that the login failed message is shown. Everything is working as expected.

In the next tutorial we shall add the registration module. Thankyou!


This Blog Post/Article "(C# ASP.NET Core) Adding a Login Page to an Identity based Authentication Project" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.