Go back

Example frontmatter

Last updated: Mon Apr 21 2025

First we will see how authentication works in a web application and what are the key terminalogies used in the asp .net core.

1. Authentication Flow — Knowing “Who You Are”

  1. User Action:

    The user enters credentials (username & password) on the login page.

  2. HTTP Request Sent:

    The browser sends these credentials in an HTTP request to the web server.

  3. Server Verification:

    • The server validates credentials against the user data store (database).
    • If correct, it creates an Identity object (user’s identity and claims).
    • This Identity forms the Security Context — the core representation of the logged-in user.
  4. Cookie Creation:

    • The Security Context is serialized and encrypted into a cookie.
    • The cookie is sent back to the browser in the HTTP response header.
    • Cookies are domain-bound; they cannot be reused across different domains for security reasons.
  5. Subsequent Requests:

    • Each new request from the browser automatically includes this cookie.
    • The server decrypts and deserializes the cookie to rebuild the Security Context.
    • This re-verification ensures the user’s identity is still valid for every request.

👉 Key idea: Authentication = establishing and recognizing who the user is.

Even after login, each request re-authenticates by re-reading the encrypted cookie.

image.png

Once authenticated 3 and 4 will repeats.


2. Authorization Flow — Knowing “What You Can Do”

  1. After authentication, the web server checks whether the Security Context grants permission to access the requested resource (page, API, or data).
  2. If authorized, the server responds with the requested data or view.
  3. If not, it returns an access-denied or redirection response.

👉 Key idea: Authorization = deciding what actions an authenticated user is allowed to perform.


4. Conceptual Hierarchy

ConceptMeaningExample
IdentityRepresents the user (username, roles, claims).User.Identity.Name
Security ContextStores the current user’s identity for the request.HTTP Context’s User object
AuthenticationVerifies who the user is.Login + cookie verification
AuthorizationDetermines what the user can access.Role/Policy checks

5. Big Picture


💡 Takeaway

Authentication establishes identity, Authorization enforces access.

ASP .Net Core Project

The cookie acts as the secure bridge between browser and server — carrying encrypted user identity across HTTP requests.

In essence, the lecture builds intuition about how ASP.NET Core identifies, tracks, and validates users before granting access.

Login.cshtml.cs

Login.cshtml.cs

@page
@model WebApplicationAuth.Pages.Account.Login

@{
    
}

<div class="container border" style="padding:20px">
    <form method="post">
        <div class="row mb-3">
            <div class="col-2">
                <label asp-for="Credential.Username"></label>
            </div>
            <div class="col-5">
                <input type="text" asp-for="Credential.Username">
            </div>
        </div>
        <div class="row mb-3">
            <div class="col-2">
                <label asp-for="Credential.Password"></label>
            </div>
            <div class="col-5">
                <input type="password" asp-for="Credential.Password">
            </div>
        </div>
    </form>
</div>

This is how we have to bind the data between the frontend and the .cs file

image.png

Terms you’ll see everywhere


Next we are goinf to add Authentication services in the Program.cs. Before that we will see the flow of the control here. Lets say, our ASP .Net Core server receives a request,

GET /UserDashboard
Cookie: .User.Cookie=ABC123; .Admin.Cookie=XYZ789

Here in the cookies header we have two values. The server side middleware must decide which authentication handler should process with the cookie. It will look for the handlers which is registered in the options. In Program.cs assume that we added the following code,

builder.Services.AddAuthentication()
    .AddCookie("UserCookieAuth", options => options.Cookie.Name = ".User.Cookie")
    .AddCookie("AdminCookieAuth", options => options.Cookie.Name = ".Admin.Cookie");
UseAuthentication()
   ├──→ Calls AuthenticateAsync() on each registered scheme
   │      ↓
   │      For "UserCookieAuth" → looks for ".User.Cookie"
   │      For "AdminCookieAuth" → looks for ".Admin.Cookie"

   └──→ First one that succeeds sets HttpContext.User

Each cookie handler does this:

  1. Reads its configured cookie name (options.Cookie.Name)
  2. Looks for that key in the incoming Cookie: header
  3. If found:
    • Extracts its value (ABC123)
    • Decrypts and validates it via Data Protection
    • Rebuilds the ClaimsPrincipal
    • Marks authentication as successful for that scheme
  4. If not found → skip silently (no cookie for me)
Browser → sends Cookie: .User.Cookie=ABC123

ASP.NET has scheme table:
  [UserCookieAuth → CookieName:.User.Cookie]
  [AdminCookieAuth → CookieName:.Admin.Cookie]

"UserCookieAuth" handler finds a match (.User.Cookie)

Decrypts, rebuilds ClaimsPrincipal

HttpContext.User = that principal

Request authenticated successfully under UserCookieAuth scheme

ASP.NET Core allows multiple auth handlers to exist, but only one will be the default authenticator — the one used to populate HttpContext.User. Here two authentication handlers are registers with a unique scheme name("UserCookieAuth" and "AdminCookieAuth") and in the each handler its own cookie name (.User.Cookie or .Admin.Cookie)

builder.Services.AddAuthentication("UserCookieAuth")
    .AddCookie("UserCookieAuth", o => o.Cookie.Name = ".User.Cookie")
    .AddCookie("AdminCookieAuth", o => o.Cookie.Name = ".Admin.Cookie");

If you have multiple cookies, you can still manually authenticate with another scheme.

var adminResult = await HttpContext.AuthenticateAsync("AdminCookieAuth");
if (adminResult.Succeeded)
{
    var adminUser = adminResult.Principal;
}

or protect routes explicitly

[Authorize(AuthenticationSchemes = "AdminCookieAuth")]

TLDR

ConceptDescription
Cookie:Raw key–value sent by browser (.User.Cookie=ABC123)
Cookie Handler:The object that knows how to decrypt and validate that cookie
Scheme Name:Identifier for that handler (UserCookieAuth)
Mapping:SchemeName → CookieName is known from Program.cs setup
Decision logic:The handler that matches the cookie name decrypts and authenticates it
Default Scheme:The one used automatically to populate HttpContext.User


2️⃣ Authentication Middleware’s Role


3️⃣ Configuring Authentication Properly


4️⃣ Default Scheme Logic


5️⃣ Authentication Handler


6️⃣ Key Takeaways

ConceptDescription
CookieEncrypted container storing user’s claims.
Scheme nameLogical key grouping all auth-related config.
MiddlewareReads cookie, decrypts, rebuilds HttpContext.User.
Order in pipelineUseAuthentication()UseAuthorization().
Default SchemeUsed automatically if only one is registered.
PurposeTo populate the Security Context for every request.

🎯 Essence

Cookie = Encrypted snapshot of your Security ContextAuthentication Middleware = The mechanism that restores itScheme = The configuration name connecting both


Authentication and Authorization Architecture

// Default authentication mechanism is mentioned in the AddCookie method. 
// Here it is MyCookieAuth
builder.Services.AddAuthentication("MyCookieAuth")
    .AddCookie("MyCookieAuth", options => 
    {
        options.Cookie.Name = "MyCookieAuth";
        options.LoginPath = "/Account/Login"; 
    });
const int probationMonths = 3;
//This is for authorization policy
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireClaim("Admin"));
    options.AddPolicy("MustBelongToHRDepartment", policy => policy.RequireClaim("Department", "HR"));
    options.AddPolicy("HRManagerOnly", policy => policy
        .RequireClaim("Department", "HR")
        .RequireClaim("Manager")
        //Below is a custom policy
        .Requirements.Add(new HrManagerProbationRequirement(probationMonths)));
});
builder.Services.AddSingleton<IAuthorizationHandler, HrManagerProbationRequirementHandler>();
// To make the cookie persistent we can set the property in AuthenticationProperties
var authProperties = new AuthenticationProperties
{
	IsPersistent = Credential.RememberMe
};
await HttpContext.SignInAsync("MyCookieAuth", principal, authProperties);

<partial> - this is to render condition based. We used it to display Login/Logout based on the user authentication.

To define custom requirement

using Microsoft.AspNetCore.Authorization;

namespace WebApp_UnderTheHood.Authorization
{
    public class HRManagerProbationRequirement : IAuthorizationRequirement
    {
        public HRManagerProbationRequirement(int probationMonths)
        {
            ProbationMonths = probationMonths;
        }

        public int ProbationMonths { get; }
    }

    public class HRManagerProbationRequirementHandler : AuthorizationHandler<HRManagerProbationRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HRManagerProbationRequirement requirement)
        {
            if (!context.User.HasClaim(x => x.Type == "EmploymentDate"))
                return Task.CompletedTask;

            if (DateTime.TryParse(context.User.FindFirst(x => x.Type == "EmploymentDate")?.Value, out DateTime employmentDate))
            {
                var period = DateTime.Now - employmentDate;
                if (period.Days > 30 * requirement.ProbationMonths)
                    context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Created a new project WebAPIController and we checked a GET request from HRManager page to Web API to fetch the weather forecast items.

For that, we have to inject the client’s (Backend) address in Program.cs and name it.

Then, create a HttpclientFactory in the respective page with the given name.

// "OurWebAPI" -> name of the client and its base address is registered
// Used in HRManager Page to GET Weather Forecasts list
builder.Services.AddHttpClient("OurWebAPI", client =>
{
    client.BaseAddress = new Uri("https://localhost:7279");
});
public class HrManager : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public HrManager(IHttpClientFactory httpClientFactory)
    {
        this._httpClientFactory = httpClientFactory;
    }

    // To display the weather details in the table we are binding 
    // It may not return a value, so it is a nullable 
    [BindProperty] 
    public List<WeatherForecastDTO>? WeatherForecastItems { get; set; }

    public async Task OnGetAsync()
    {
        var client = _httpClientFactory.CreateClient("OurWebAPI");
        WeatherForecastItems = await client.GetFromJsonAsync<List<WeatherForecastDTO>>("WeatherForecast");
        // WeatherForeCast is the controller name. Routing is based on the controller name.
    }
}

This is how we call APIs of other projects. We used DTOs to map the objects.

Token Based Authentication

image.png

Authenticate method in a controller

Here the username and password are hardcoded for simplicity. If valid credentials are received, we are returing token if not we are returning 401 Unauthorized. We can see that the steps are almost similar to the cookies based authentication.

//WebAPI/Controllers/AuthController.cs
 private readonly IConfiguration _configuration;
    //Constructor
    public AuthController(IConfiguration configuration)
    {
        this._configuration = configuration; // To read Secrete key
    }
    
[HttpPost]
        public IActionResult Authenticate([FromBody]Credential credential)
        {
            if (credential.Username == "admin" && credential.Password == "password")
            {
                // Creating the security context
                var claims = new List<Claim> {
                    new Claim(ClaimTypes.Name, "admin"),
                    new Claim(ClaimTypes.Email, "admin@mywebsite.com"),
                    new Claim("Department", "HR"),
                    new Claim("Admin", "true"),
                    new Claim("Manager", "true"),
                    new Claim("EmploymentDate", "2025-01-01")
                };
                
                var expiresAt = DateTime.UtcNow.AddMinutes(10);

                return Ok(new
                {
                    access_token = CreateToken(claims, expiresAt),
                    expires_at = expiresAt,
                });
            }

            ModelState.AddModelError("Unauthorized", "You are not authorized to access the endpoint.");
            var problemDetails = new ProblemDetails
            {
                Title = "Unauthorized",
                Status = StatusCodes.Status401Unauthorized,                
            };
            return Unauthorized(problemDetails);
        }

How to create a token

Required configurations are

Token handlers create JWT with the help of the above mentioned configs. In in the below code, claims are added in dictionary and then passed to the token handler.

//WebAPI/Controllers/AuthController.cs
private string CreateToken(List<Claim> claims, DateTime expiresAt)
    {
        var claimsDic = new Dictionary<string, object>();
        if (claims is not null && claims.Count > 0)
        {
            foreach (var claim in claims)
            {
                claimsDic.Add(claim.Type, claim.Value);
            }
        }

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(
                    System.Text.Encoding.UTF8.GetBytes(configuration["SecretKey"] ?? string.Empty)),
                SecurityAlgorithms.HmacSha256Signature),
            Claims = claimsDic,
            Expires = expiresAt
            NotBefore = DateTime.UtcNow,
        };

        var tokenHandler = new JsonWebTokenHandler();
        return tokenHandler.CreateToken(tokenDescriptor);
    }

Reading Tokens

We have already seen how to create and send a token from the server to the client.

The following example shows how to register or inject it in

For authorization, we added a policy named AdminOnly, which requires the presence of the Adminclaim.

var secretKey = builder.Configuration["SecretKey"];
// Install Authentication.JwtBearer Nuget Package
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey??string.Empty)),
        ClockSkew = TimeSpan.Zero // Disable the default clock skew of 5 minutes
    };
});
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireClaim("Admin"));
});

Admin claim was already included in the token response. Therefore, in subsequent requests, we need to check for this claim.

//WebApplicationAuth/Pages/HRManager.cshtml.cs
//......
[Authorize(policy:"AdminOnly")]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
//....
}

Once we have implemented JWT generation on the server side, the next step is to consume it from the client and use it to access protected API endpoints.

Here’s how we can do that in the OnGetAsync() method:

//WebApplicationAuth/Pages/HRManager.cshtml.cs
public async Task OnGetAsync()
    {
        // Create an HTTP client instance using the registered factory
        var httpClient = _httpClientFactory.CreateClient("OurWebAPI");
        
        // Step 1: Authenticate the user and get a JWT
        // TODO: Fetch Username and Password from forms
        var response = await httpClient.PostAsJsonAsync(
        "Auth", new Credentials {Username = "admin", Password = "password"});
        // Ensure the response was successful
        response.EnsureSuccessStatusCode();
        // Step 2: Read and deserialize the JWT token
        var strJwt = await response.Content.ReadAsStringAsync();
        var token = JsonConvert.DeserializeObject<JwtToken>(strJwt);
        // Step 3: Attach the token to the Authorization header
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Bearer", token?.access_token?? string.Empty);
        // Step 4: Use the token to call a protected API endpoint
        WeatherForecastItems = await httpClient.
        GetFromJsonAsync<List<WeatherForecastDTO>>("WeatherForecast");
    }

Store and Reuse Token in Session

When working with APIs secured by JWT tokens, it’s inefficient to request a new token every time you make an API call. Instead, you can store the token in session and reuse it until it expires. Here’s how to do that.

Step 1: Add sessions support in Program.cs

builder.Services.AddSession(options =>
{
    options.Cookie.HttpOnly = true;
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.IsEssential = true;
});

Then add session middleware

app.UseSession();

Step 2: Retrieve and Store Token in the Page Model

In your Razor Page or Controller, implement logic to check if the token exists in session.

If not, authenticate to get a new token and store it for reuse.

public async Task OnGetAsync()
{
    JwtToken token = new JwtToken();

    // Try to retrieve token from session
    var strTokenObj = HttpContext.Session.GetString("access_token");
    if (string.IsNullOrEmpty(strTokenObj))
    {
        token = await Authenticate();
    }
    else
    {
        token = JsonConvert.DeserializeObject<JwtToken>(strTokenObj) ?? new JwtToken();
    }

    // If token is invalid or expired, re-authenticate
    if (token == null ||
        string.IsNullOrWhiteSpace(token.AccessToken) ||
        token.ExpiresAt <= DateTime.UtcNow)
    {
        token = await Authenticate();
    }

    // Use token for authorized API call
    var httpClient = httpClientFactory.CreateClient("OurWebAPI");
    httpClient.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", token?.AccessToken ?? string.Empty);

    weatherForecastItems =
        await httpClient.GetFromJsonAsync<List<WeatherForecastDTO>>("WeatherForecast")
        ?? new List<WeatherForecastDTO>();
}

Step 3: Authenticate and Save Token in Session

The Authenticate() method requests a token from your API and saves it in the session for future use.

private async Task<JwtToken> Authenticate()
{
    var httpClient = httpClientFactory.CreateClient("OurWebAPI");

    // Send credentials to authenticate
    var res = await httpClient.PostAsJsonAsync("auth", new Credential
    {
        UserName = "admin",
        Password = "password"
    });

    res.EnsureSuccessStatusCode();

    // Deserialize token and store in session
    string strJwt = await res.Content.ReadAsStringAsync();
    var token = JsonConvert.DeserializeObject<JwtToken>(strJwt);
    HttpContext.Session.SetString("access_token", strJwt);

    // Example API call using the new token
    httpClient.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", token?.AccessToken ?? string.Empty);

    weatherForecastItems =
        await httpClient.GetFromJsonAsync<List<WeatherForecastDTO>>("WeatherForecast")
        ?? new List<WeatherForecastDTO>();

    return token ?? new JwtToken();
}

ASP .Net Core Identity

ASP.NET Core Identity:

Three essential parts of Identity

Required Packages

Microsoft.AspNetCore.Identity.EntityFrameworkCore

Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore.Design

Microsoft.EntityFrameworkCore.Tools

Microsoft.EntityFrameworkCore.SqlServer

Migrations

dotnet ef migrations add InitialCreate dotnet ef database update

// Import required namespaces
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;

namespace WebApp.Pages.Account
{
    // Razor PageModel for handling User Registration
    public class RegisterModel : PageModel
    {
        // The UserManager class is a part of ASP.NET Core Identity
        // It provides APIs to manage users: create, update, delete, validate passwords, etc.
        private readonly UserManager<IdentityUser> _userManager;

        // Constructor injection for UserManager via Dependency Injection
        public RegisterModel(UserManager<IdentityUser> userManager)
        {
            _userManager = userManager;
        }

        // BindProperty allows automatic two-way data binding between
        // Razor Page input fields and this property during form submission.
        [BindProperty]
        public RegisterViewModel registerViewModel { get; set; } = new();

        // Called when the Register page is requested via HTTP GET
        // Currently empty since no preloading or initialization is required
        public void OnGet() { }

        // Handles the POST request when the user submits the registration form
        // The 'async' keyword is used because UserManager methods are asynchronous
        public async Task<IActionResult> OnPostAsync()
        {
            // Step 1: Validate the model — ensure all required fields are valid
            if (!ModelState.IsValid)
            {
                // Return the same page with validation error messages
                return Page();
            }

            // Step 2: Create a new IdentityUser instance
            // IdentityUser is the default user entity provided by ASP.NET Core Identity
            // It includes properties like Email, UserName, Id, etc.
            var identityUser = new IdentityUser
            {
                Email = registerViewModel.Email,        // Assign email from form input
                UserName = registerViewModel.Email      // Using Email as the username
            };

            // Step 3: Attempt to create the user in the Identity system
            // This also performs password policy validation (length, uppercase, etc.)
            var result = await _userManager.CreateAsync(identityUser, registerViewModel.Password);

            // Step 4: Handle result
            if (result.Succeeded)
            {
                // If registration is successful, redirect the user to the Index page
                // (You can change this to redirect to Login or Confirmation page)
                return RedirectToPage("/Index");
            }
            else
            {
                // If creation failed, Identity returns one or more error messages
                // Loop through all errors and add them to ModelState so Razor can display them
                foreach (var error in result.Errors)
                {
                    // Each error has a Code (e.g., DuplicateEmail) and Description (e.g., "Email already taken")
                    ModelState.AddModelError(error.Code, error.Description);
                }

                // Return the same page with validation errors displayed
                return Page();
            }
        }
    }

    // ViewModel for capturing registration input fields from the user
    public class RegisterViewModel
    {
        // Email field is required and must be in valid email format
        [Required(ErrorMessage = "Email is required")]
        [EmailAddress(ErrorMessage = "Invalid email address")]
        public string Email { get; set; }

        // Password field is required and masked in the UI due to DataType.Password
        [Required(ErrorMessage = "Password is required")]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
ConceptPurpose
UserManager<IdentityUser>Core Identity service that handles user creation, validation, password rules, and persistence.
[BindProperty]Enables two-way binding between Razor Page form fields and server-side properties.
ModelState.IsValidEnsures that all validation attributes (like [Required], [EmailAddress]) pass before proceeding.
_userManager.CreateAsync()Creates a new user and automatically hashes the password before saving to the database.
ModelState.AddModelError()Collects error messages from Identity (e.g., duplicate email, weak password) and displays them in Razor.
RedirectToPage("/Index")Redirects the user after successful registration. Usually changed later to /Account/Login or /Account/ConfirmEmail.