Search  
Always will be ready notify the world about expectations as easy as possible: job change page
Jun 1

ASP.NET 8 Token Authentication for Web API and React with Integration Testing (Part 1: API)

Source:
Views:
3543

Welcome to the first instalment of our comprehensive guide on securing your web applications with token authentication using ASP.NET Identity in .NET 8. In this part, we delve into the backbone of our authentication system — the Web API. We will walk you through the initial setup of your ASP.NET 8 project, demonstrate how to configure ASP.NET Identity for token-based authentication and explain the creation of secure endpoints. By the end of this article, you’ll have a robust API, ready to authenticate users and serve as a solid foundation for your application’s backend. Whether you’re a seasoned developer or new to ASP.NET, this guide will equip you with the knowledge to implement a secure authentication system in your web applications.

Source Code

What we are going to achieve?

ASP.NET 8 API

API Swagger
Swagger

React application with Redux and Ant Design

Home

Login
Home & Login

Token

Register
Token & Register

Integration tests

Integration tests
Integration Tests

This article will be presented in three parts

Part 1: Setting up API
Part 2: Setting up integration tests for API project
Part 3: Setting up React client

Ready to get started? We’ll walk through the foundational elements of token authentication and then move on to the practical steps for integrating it into your web application.

Project overview

Our application is built on a clear, modular structure, comprised of four key projects: API, Data, Service, and Web. This structure not only promotes a clean separation of concerns but also ensures a secure, efficient, and user-friendly application:

  1. API project: Serves as the gateway for frontend-backend communication, managing HTTP requests and integrating token-based authentication.
  2. Data project: Handles all database interactions, defining models and ensuring data integrity.
  3. Service project: Processes business logic, interacts with the Data project and handles user authentication and token management.
  4. Web project: Delivers the frontend interface, managing state with Redux, handling authentication, and communicating with the backend via API calls.
  5. ApiTest project: Integration test project for API.

We’ll begin with the Data project, the heart of our application’s data handling. This class library provides the essential mechanisms for managing our data.

• • •

Data project: Data management

The Data project is a class library in our application that serves as the core for managing data interactions.

Data project classes

ApplicationUser: Extends the IdentityUser class, defining the properties of a user in the system.

public class ApplicationUser : IdentityUser
{
}

ApplicationDbContext: Inherits from IdentityDbContext, handling the database context and configurations for user management.

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options)
{
}

ApplicationDbContextInitialiser: Responsible for initializing the database and seeding it with initial data like default users and roles.

public class ApplicationDbContextInitialiser(ILogger<ApplicationDbContextInitialiser> logger, ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
    private readonly ILogger<ApplicationDbContextInitialiser> _logger = logger;
    private readonly ApplicationDbContext _context = context;
    private readonly UserManager<ApplicationUser> _userManager = userManager;
    private readonly RoleManager<IdentityRole> _roleManager = roleManager;

    public async Task InitialiseAsync()
    {
        try
        {
            if (_context.Database.IsSqlServer())
            {
                await _context.Database.MigrateAsync();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while initialising the database.");
            throw;
        }
    }

    public async Task SeedAsync()
    {
        try
        {
            await TrySeedAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while seeding the database.");
            throw;
        }
    }

    public async Task TrySeedAsync()
    {
        // Default roles
        var administratorRole = new IdentityRole("Administrator");

        if (_roleManager.Roles.All(r => r.Name != administratorRole.Name))
        {
            var role = await _roleManager.CreateAsync(administratorRole);
            if (role != null)
            {
                await _roleManager.AddClaimAsync(administratorRole, new Claim("RoleClaim", "HasRoleView"));
                await _roleManager.AddClaimAsync(administratorRole, new Claim("RoleClaim", "HasRoleAdd"));
                await _roleManager.AddClaimAsync(administratorRole, new Claim("RoleClaim", "HasRoleEdit"));
                await _roleManager.AddClaimAsync(administratorRole, new Claim("RoleClaim", "HasRoleDelete"));
            }
        }

        // Default users
        var administrator = new ApplicationUser { UserName = "UnifiedAppAdmin", Email = "UnifiedAppAdmin" };

        if (_userManager.Users.All(u => u.UserName != administrator.UserName))
        {
            await _userManager.CreateAsync(administrator, "UnifiedAppAdmin1!");
            if (!string.IsNullOrWhiteSpace(administratorRole.Name))
            {
                await _userManager.AddToRolesAsync(administrator, new[] { administratorRole.Name });
            }
        }
    }
}

Perfect! This basic setup for the data layer.

• • •

Service project: Authentication and Authorization business logic

The Service project, an integral part of our application, specializes in Authentication and Authorization business logic.

Service project classes

TokenSettings class is designed to configure parameters for authentication tokens.

public class TokenSettings
{
    public string Issuer { get; set; } = "";
    public string Audience { get; set; } = "";
    public string SecretKey { get; set; } = "";
    public int TokenExpireSeconds { get; set; }
    public int RefreshTokenExpireSeconds { get; set; }
    public bool ValidateIssuer { get; set; } = true;
    public bool ValidateAudience { get; set; } = true;
    public bool ValidateLifetime { get; set; } = true;
}

AppResponse<T> class, a fundamental component for handling responses within our application. This class is designed to manage and structure responses, whether they are successful or error-related, providing a standardized format for service layer responses.

public class AppResponse<T>
{
    public bool IsSucceed { get; private set; } = true;
    public Dictionary<string, string[]> Messages { get; private set; } = [];
    public T? Data { get; private set; }
    
    public AppResponse<T> SetSuccessResponse(T data)
    {
        Data = data;
        return this;
    }
    
    public AppResponse<T> SetSuccessResponse(T data, string key, string value)
    {
        Data = data;
        Messages.Add(key, [value]);
        return this;
    }
    
    public AppResponse<T> SetSuccessResponse(T data, Dictionary<string, string[]> message)
    {
        Data = data;
        Messages = message;
        return this;
    }
    
    public AppResponse<T> SetSuccessResponse(T data, string key, string[] value)
    {
        Data = data;
        Messages.Add(key, value);
        return this;
    }
    
    public AppResponse<T> SetErrorResponse(string key, string value)
    {
        IsSucceed = false;
        Messages.Add(key, [value]);
        return this;
    }
    
    public AppResponse<T> SetErrorResponse(string key, string[] value)
    {
        IsSucceed = false;
        Messages.Add(key, value);
        return this;
    }
    
    public AppResponse<T> SetErrorResponse(Dictionary<string, string[]> message)
    {
        IsSucceed = false;
        Messages = message;
        return this;
    }
}

TokenUtil class serves as a static utility class central to the management of authentication tokens within our application. Its primary purpose is to provide essential functionality for the creation and validation of JSON Web Tokens (JWTs), which are integral to user authentication and authorization processes.

public static class TokenUtil
{
    public static string GetToken(TokenSettings appSettings, ApplicationUser user, List<Claim> roleClaims)
    {
        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.SecretKey));
        var signInCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

        var userClaims = new List<Claim>
        {
            new("Id", user.Id.ToString()),
            new ("UserName", user.UserName??"")
        };
        userClaims.AddRange(roleClaims);
        var tokeOptions = new JwtSecurityToken(
            issuer: appSettings.Issuer,
            audience: appSettings.Audience,
            claims: userClaims,
            expires: DateTime.UtcNow.AddSeconds(appSettings.TokenExpireSeconds),
            signingCredentials: signInCredentials
        );
        var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
        return tokenString;
    }

    public static ClaimsPrincipal GetPrincipalFromExpiredToken(TokenSettings appSettings, string token)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidAudience = appSettings.Audience,
            ValidIssuer = appSettings.Issuer,
            ValidateLifetime = false,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.SecretKey))
        };

        var principal = new JwtSecurityTokenHandler().ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
        if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("GetPrincipalFromExpiredToken Token is not validated");

        return principal;
    }
}

Methods:

GetToken: responsible for the generation of JWT tokens.

GetPrincipalFromExpiredToken: validation of expired JWT tokens and the extraction of their claims to create a ClaimsPrincipal instance.

UserService class is a crucial component in our application, responsible for managing user-related operations, authentication, and token generation.

public partial class UserService(UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    RoleManager<IdentityRole> roleManager,
    ApplicationDbContext applicationDbContext,
    TokenSettings tokenSettings)
{
    private readonly UserManager<ApplicationUser> _userManager = userManager;
    private readonly SignInManager<ApplicationUser> _signInManager = signInManager;
    private readonly RoleManager<IdentityRole> _roleManager = roleManager;
    private readonly TokenSettings _tokenSettings = tokenSettings;
    private readonly ApplicationDbContext _context = applicationDbContext;

    private async Task<UserLoginResponse> GenerateUserToken(ApplicationUser user)
    {
        var claims = (from ur in _context.UserRoles
                      where ur.UserId == user.Id
                      join r in _context.Roles on ur.RoleId equals r.Id
                      join rc in _context.RoleClaims on r.Id equals rc.RoleId
                      select rc)
          .Where(rc => rc.ClaimValue != null && rc.ClaimType != null)
          .Select(rc => new Claim(rc.ClaimType ?? "", rc.ClaimValue ?? ""))
          .Distinct()
          .ToList();
        var token = TokenUtil.GetToken(_tokenSettings, user, claims);
        await _userManager.RemoveAuthenticationTokenAsync(user, "REFRESHTOKEN", "RefreshToken");
        var refreshToken = await _userManager.GenerateUserTokenAsync(user, "REFRESHTOKEN", "RefreshToken");
        await _userManager.SetAuthenticationTokenAsync(user, "REFRESHTOKEN", "RefreshToken", refreshToken);
       
        return new UserLoginResponse() { AccessToken = token, RefreshToken = refreshToken };
    }
}

• • •

Constructor parameters:

UserManager<ApplicationUser>: Manages user-related operations and interactions with the user store.
SignInManager<ApplicationUser>: Handles user sign-in and sign-out processes.
RoleManager<IdentityRole>: Provides functionalities for managing user roles.
ApplicationDbContext: Represents the database context for interacting with user data.
TokenSettings: Configures parameters for authentication tokens, such as issuer, audience, and expiration times.

Methods:

GenerateUserToken: This method generates authentication tokens for a given user. It retrieves the user’s roles and corresponding claims from the database. Using the TokenUtil class, it then generates an access token and a refresh token. The access token is returned to the user, and the refresh token is stored securely using the UserManager. The method returns a UserLoginResponse object containing the generated access and refresh tokens.

UserRegisterAsync: method allows users to register by providing registration details.

public class UserRegisterRequest
{
    public string Email { get; set; } = "";
    public string Password { get; set; } = "";
}

public partial class UserService
{
    public async Task<AppResponse<bool>> UserRegisterAsync(UserRegisterRequest request)
    {
        var user = new ApplicationUser()
        {
            UserName = request.Email,
            Email = request.Email,

        };
        var result = await _userManager.CreateAsync(user, request.Password);
        if (result.Succeeded)
        {
            return new AppResponse<bool>().SetSuccessResponse(true);
        }
        else
        {
            return new AppResponse<bool>().SetErrorResponse(GetRegisterErrors(result));
        }
    }

    private Dictionary<string, string[]> GetRegisterErrors(IdentityResult result)
    {
        var errorDictionary = new Dictionary<string, string[]>(1);

        foreach (var error in result.Errors)
        {
            string[] newDescriptions;

            if (errorDictionary.TryGetValue(error.Code, out var descriptions))
            {
                newDescriptions = new string[descriptions.Length + 1];
                Array.Copy(descriptions, newDescriptions, descriptions.Length);
                newDescriptions[descriptions.Length] = error.Description;
            }
            else
            {
                newDescriptions = [error.Description];
            }

            errorDictionary[error.Code] = newDescriptions;
        }

        return errorDictionary;
    }
}

UserLoginAsync: This method facilitates user login by verifying the provided email and password against stored credentials.

public class UserLoginRequest
{
    public string Email { get; set; } = "";
    public string Password { get; set; } = "";
}

public class UserLoginResponse
{
    public string AccessToken { get; set; } = "";
    public string RefreshToken { get; set; } = "";
}

public partial class UserService
{
    public async Task<AppResponse<UserLoginResponse>> UserLoginAsync(UserLoginRequest request)
    {
        var user = await _userManager.FindByEmailAsync(request.Email);
        if (user == null)
        {

            return new AppResponse<UserLoginResponse>().SetErrorResponse("email", "Email not found");
        }
        else
        {
            var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, true);
            if (result.Succeeded)
            {
                var token = await GenerateUserToken(user);
                return new AppResponse<UserLoginResponse>().SetSuccessResponse(token);
            }
            else
            {
                return new AppResponse<UserLoginResponse>().SetErrorResponse("password", result.ToString());
            }
        }
    }
}

UserRefreshTokenAsync: This method is responsible for refreshing user authentication tokens using a provided access token and refresh token.

public class UserRefreshTokenRequest
{
    public string AccessToken { get; set; } = "";
    public string RefreshToken { get; set; } = "";
}

public class UserRefreshTokenResponse
{
    public string AccessToken { get; set; } = "";
    public string RefreshToken { get; set; } = "";
}

public partial class UserService
{
    public async Task<AppResponse<UserRefreshTokenResponse>> UserRefreshTokenAsync(UserRefreshTokenRequest request)
    {
        var principal = TokenUtil.GetPrincipalFromExpiredToken(_tokenSettings, request.AccessToken);
        if (principal == null || principal.FindFirst("UserName")?.Value == null)
        {
            return new AppResponse<UserRefreshTokenResponse>().SetErrorResponse("email", "User not found");
        }
        else
        {
            var user = await _userManager.FindByNameAsync(principal.FindFirst("UserName")?.Value ?? "");
            if (user == null)
            {
                return new AppResponse<UserRefreshTokenResponse>().SetErrorResponse("email", "User not found");
            }
            else
            {
                if (!await _userManager.VerifyUserTokenAsync(user, "REFRESHTOKENPROVIDER", "RefreshToken", request.RefreshToken))
                {
                    return new AppResponse<UserRefreshTokenResponse>().SetErrorResponse("token", "Refresh token expired");
                }
                var token = await GenerateUserToken(user);
               
                return new AppResponse<UserRefreshTokenResponse>().SetSuccessResponse(new UserRefreshTokenResponse() { AccessToken = token.AccessToken, RefreshToken = token.RefreshToken });
            }
        }
    }
}

UserLogoutAsync: This method handles user logout by updating the security stamp of the authenticated user.

public partial class UserService
{
    public async Task<AppResponse<bool>> UserLogoutAsync(ClaimsPrincipal user)
    {
        if (user.Identity?.IsAuthenticated ?? false)
        {
            var username = user.Claims.First(x => x.Type == "UserName").Value;
            var appUser = _context.Users.First(x => x.UserName == username);
            if (appUser != null)
            {
                await _userManager.UpdateSecurityStampAsync(appUser);
            }

            return new AppResponse<bool>().SetSuccessResponse(true);
        }
       
        return new AppResponse<bool>().SetSuccessResponse(true);
    }
}

Ok! Let’s move to the API project

• • •

API project: Exposing endpoints

API Project is responsible for exposing endpoints that facilitate communication between the front-end and back-end. Endpoints serve as designated entry points for handling various types of HTTP requests.

API project classes

UserController, which handles user-related operations.

[ApiController]
[Route("[controller]/[action]")]
public class UserController(UserService userService) : ControllerBase
{
    private readonly UserService _userService = userService;

    [HttpPost]
    public async Task<AppResponse<bool>> Register(UserRegisterRequest req)
    {
        return await _userService.UserRegisterAsync(req);
    }

    [HttpPost]
    public async Task<AppResponse<UserLoginResponse>> Login(UserLoginRequest req)
    {
        return await _userService.UserLoginAsync(req);
    }

    [HttpPost]
    public async Task<AppResponse<UserRefreshTokenResponse>> RefreshToken(UserRefreshTokenRequest req)
    {
        return await _userService.UserRefreshTokenAsync(req);
    }
    
    [HttpPost]
    public async Task<AppResponse<bool>> Logout()
    {
        return await _userService.UserLogoutAsync(User);
    }

    [HttpPost]
    [Authorize]
    public string Profile()
    {
        return User.FindFirst("UserName")?.Value ?? "";
    }
}

Register endpoint: accessed via a POST request, allows users to register in the system.

Login endpoint: facilitates user login by processing a POST request.

RefreshToken endpoint: processes a POST request. It collaborates with the UserService to asynchronously refresh the user’s authentication token.

Logout endpoint: triggered by a POST request, allows users to log out.

Profile endpoint: accessible with proper authorization, retrieves the user’s profile information. This endpoint is protected with the [Authorize] attribute, ensuring that only authenticated users can access it.

This is the Application settings (appsettings.json) file.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=UnifiedAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "TokenSettings": {
    "Issuer": "https://localhost:1002/",
    "Audience": "http://localhost:1000",
    "SecretKey": "**You can add your own secret key**",
    "TokenExpireSeconds": 3600,
    "RefreshTokenExpireSeconds": 25200
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

TokenSettings section:
The TokenSettings section contains configuration settings related to authentication tokens in your application:

Issuer: Specifies the issuer of the token, which is the identity provider.
Audience: Represents the audience for which the token is intended.
SecretKey: Provides the secret key used for signing the tokens. It is crucial for token validation and security.
TokenExpireSeconds: Sets the expiration time for the authentication token in seconds. The value “3600” indicates a token expiration of one hour (60 seconds * 60 minutes).
RefreshTokenExpireSeconds: Specifies the expiration time for the refresh token in seconds. The value “25200” indicates a refresh token expiration of seven hours (60 seconds * 60 minutes * 7 hours).

Program file

The last piece of the API application is the Program.cs file, which is where we resolve all our dependencies for the application.

using Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Service;
using Service.UserGroup;
using System.Text;

namespace Api
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            var connectionStr = builder.Configuration.GetConnectionString("DefaultConnection");
            var appSettings = builder.Configuration.GetSection("TokenSettings").Get<TokenSettings>() ?? default!;
            builder.Services.AddSingleton(appSettings);

            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                            options.UseSqlServer(connectionStr, x => x.MigrationsAssembly("Data")));

            builder.Services.AddIdentityCore<ApplicationUser>()
                .AddRoles<IdentityRole>()
                .AddSignInManager()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddTokenProvider<DataProtectorTokenProvider<ApplicationUser>>("REFRESHTOKENPROVIDER");

            builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
            {
                options.TokenLifespan = TimeSpan.FromSeconds(appSettings.RefreshTokenExpireSeconds);
            });

            builder.Services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        RequireExpirationTime = true,
                        ValidIssuer = appSettings.Issuer,
                        ValidAudience = appSettings.Audience,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.SecretKey)),
                        ClockSkew = TimeSpan.FromSeconds(0)
                    };
                });

            builder.Services.AddScoped<ApplicationDbContextInitialiser>();
            builder.Services.AddTransient<UserService>();
            builder.Services.AddControllers();
            builder.Services.AddEndpointsApiExplorer();

            builder.Services.AddCors(options =>
            {
                options.AddPolicy("webAppRequests", builder =>
                {
                    builder.AllowAnyHeader()
                    .AllowAnyMethod()
                    .WithOrigins(appSettings.Audience)
                    .AllowCredentials();
                });
            });

            builder.Services.AddSwaggerGen(config =>
            {
                config.SwaggerDoc("v1", new OpenApiInfo() { Title = "App Api", Version = "v1" });
                config.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    In = ParameterLocation.Header,
                    Description = "Please enter token",
                    Name = "Authorization",
                    Type = SecuritySchemeType.Http,
                    BearerFormat = "JWT",
                    Scheme = "bearer"
                });
                config.AddSecurityRequirement(
                    new OpenApiSecurityRequirement{
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type=ReferenceType.SecurityScheme,
                                Id="Bearer"
                            }
                        },
                        Array.Empty<string>()
                    }
                });
            });

            var app = builder.Build();
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
                using var scope = app.Services.CreateScope();
                var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();
                await initialiser.InitialiseAsync();
                await initialiser.SeedAsync();
            }
            app.UseHttpsRedirection();
            app.UseCors("webAppRequests");
            app.UseAuthentication();
            app.UseAuthorization();
            app.MapControllers();
            app.Run();
        }
    }
}

Let’s take it one step at a time and see…

var connectionStr = builder.Configuration.GetConnectionString("DefaultConnection");
var appSettings = builder.Configuration.GetSection("TokenSettings").Get<TokenSettings>() ?? default!;
builder.Services.AddSingleton(appSettings);

We retrieve the database connection string and token settings from the application’s configuration. Then token settings are registered as a singleton for consistent access throughout the application.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(connectionStr, x => x.MigrationsAssembly("Data")));

In this line, we configure the application’s database context (ApplicationDbContext) to use SQL Server, specifying the connection string retrieved earlier. Additionally, it includes migration assembly information.

builder.Services.AddIdentityCore<ApplicationUser>();

This line configures the core identity services for your application, centered around the ApplicationUser class.

.AddRoles<IdentityRole>()
.AddSignInManager()
.AddEntityFrameworkStores<ApplicationDbContext>()

These lines extend the identity services by adding roles, setting up sign-in management, and specifying the storage mechanism using ApplicationDbContext.

.AddTokenProvider<DataProtectorTokenProvider<ApplicationUser>>("REFRESHTOKENPROVIDER");

This line introduces a token provider (DataProtectorTokenProvider) named ‘REFRESHTOKENPROVIDER’.

builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
{
    options.TokenLifespan = TimeSpan.FromSeconds(appSettings.RefreshTokenExpireSeconds);
});

This snippet configures the lifespan of tokens.

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            RequireExpirationTime = true,
            ValidIssuer = appSettings.Issuer,
            ValidAudience = appSettings.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.SecretKey)),
            ClockSkew = TimeSpan.FromSeconds(0)
        };
    });

In this block, We set up authentication for the API using JWT (JSON Web Token). The AddAuthentication method configures the default authentication and challenge schemes to use JWT. The AddJwtBearer method further specifies the parameters for validating the incoming JWT tokens. It includes settings to validate the issuer, audience, lifetime, and cryptographic signature of the tokens. Additionally, it specifies the issuer, audience, and key used for token validation, along with other relevant options. This setup ensures secure and reliable authentication for the API using JWT tokens.

With these configurations in place, our API project is now well-prepared with an identity and authentication mechanism.

builder.Services.AddScoped<ApplicationDbContextInitialiser>();
builder.Services.AddTransient<UserService>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

We register the database initializer and user service as scoped and transient services, respectively, to manage the application’s data and user-related operations. Controllers are added to handle various endpoints, and Swagger/OpenAPI documentation is configured for enhanced API exploration.

builder.Services.AddCors(options =>
{
    options.AddPolicy("webAppRequests", builder =>
    {
        builder.AllowAnyHeader()
        .AllowAnyMethod()
        .WithOrigins(appSettings.Audience)
        .AllowCredentials();
    });
});

builder.Services.AddSwaggerGen(config =>
{
    config.SwaggerDoc("v1", new OpenApiInfo() { Title = "App Api", Version = "v1" });
    config.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "bearer"
    });
    config.AddSecurityRequirement(
        new OpenApiSecurityRequirement{
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            Array.Empty<string>()
        }
    });
});

We configure Cross-Origin Resource Sharing (CORS) to allow web application requests with specific headers, methods, origins, and credentials. Additionally, Swagger/OpenAPI documentation is set up, providing a comprehensive API reference with security definitions for token-based authentication.

The remaining configurations are straightforward, and you can easily customize them based on your requirements. Feel free to use the comment section if you have any questions or need further assistance — I’m happy to help!

Summary

In this coding journey, we navigated through the creation of an ASP.NET Core application with a focus on authentication and API development. We covered the setup of essential components, including database connections, token settings, and Identity services. The authentication mechanism was implemented using JWT tokens. The API project exposed endpoints for user registration, login, token refresh, and more. We configured Swagger for API documentation and enabled CORS for cross-origin requests. The program file orchestrated the dependency injection, authentication, and authorization. Overall, this journey provides a foundational guide for building a secure and functional ASP.NET Core API.

Source Code

For the complete code and more examples, check out the GitHub repository: UnifiedApp

Similar
Sep 10, 2023
Author: Sriram Kumar Mannava
In a situation where we need to modify our API’s structure or functionality while ensuring that existing API clients remain unaffected, the solution is versioning. We can designate our current APIs as the older version and introduce all intended changes...
Nov 25, 2022
Author: Amit Naik
In this article, we will see Distributed caching, Redis cache, and also Redis caching in ASP.NET Core. Follow me on Github and Download source code from GitHub Table of Content What is distributed caching and its benefit IDistributedCache interface Framework...
Oct 26, 2023
Author: Matt Bentley
How to implement CQRS in ASP.NET using MediatR. A guided example using CQRS with separate Read and Write models using Enity Framework Core for Commands and Dapper for Queries. When people think about CQRS they often think about complex, event-driven,...
Jun 1
Author: Akalanka Dissanayake
In the second part of our series, the focus shifts towards validating the security and reliability of our ASP.NET 8 Web API through comprehensive integration testing. Integration testing plays a critical role in ensuring that our authentication mechanisms work as...
Send message
Type
Email
Your name
*Message