Implementing Login: Mac + .NET 5 + Identity + JWT + VS Code

Implementing Login: Mac + .NET 5 + Identity + JWT + VS Code

Sharing a small login case study I learned earlier

Last updated 10/18/2021 4:51 PM
Jarry
10 min read
Category
ASP.NET Core
Tags
.NET C# ASP.NET Core Web API Authentication

Sharing a login demo case I learned earlier. If there are any shortcomings in the code, please feel free to point them out!

Tools: Developed using VS Code and its plugins for lightweight development while reducing command line typing. No conflicts with VS!

1. Create a WebApi Project via Plugin

The original text has a GIF; click the original post to view it

2. Download NuGet Packages Required by the Project Using the Plugin

3. Code Writing

① Create a User Entity

/// <summary>
/// Login user entity class, inherits IdentityUser from the Identity framework
/// </summary>
public class AppUser:IdentityUser
{
    // Add three additional fields
    public DateTime DateCreated { get; set; }
    public DateTime DateModified { get; set; }

    public string FullName { get; set; }
}

② Create a DbContext Class

public class AppDBContext : IdentityDbContext<AppUser, IdentityRole, string>
{
    public AppDBContext(DbContextOptions options) : base(options)
    {
    }
}

③ Inject the DbContext in Startup

services.AddDbContext<AppDBContext>(options =>
{
    options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), MySqlServerVersion.LatestSupportedServerVersion);
});
// AddEntityFrameworkStores is used to create services between users and passwords
services.AddIdentity<AppUser, IdentityRole>(opt => { }).AddEntityFrameworkStores<AppDBContext>();

④ Generate Database Tables via Code-First in Terminal

dotnet ef migrations add init
dotnet ef  database update

⑤ Configure JWT

Configure services in the ConfigureServices method

services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddJwtBearer(options =>
    {
        // The JWT key needs to be complex
        var key = Encoding.ASCII.GetBytes(Configuration["JWTConfig:Key"]);
        var issure = Configuration["JWTConfig:Issuer"];   // Issuer
        var audience = Configuration["JWTConfig:Audience"];  // Audience
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = true, // If set to true, set the ValidIssuer property; otherwise JWT validation will fail
            ValidateAudience = true, // Same as above, set the ValidAudience property
            RequireExpirationTime = true,
            ValidateLifetime=true,   // Token expiration buffer time, default 5 minutes; expiration time needs to include this 5-minute buffer
            // If ValidateIssuer is set to false, the following two properties are not needed
            ValidIssuer = issure,
            ValidAudience = audience,

        };
    });
// For multiple roles, configure as follows. It can be simplified on action methods: [Authorize(Policy ="PolicyGroup")]
services.AddAuthorization(options =>
{
    options.AddPolicy("PolicyGroup", policy => policy.RequireRole("Admin", "User"));
});

Use services in the Configure method

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerDemo v1"));
    }

    app.UseHttpsRedirection();
    app.UseCors("any");
    app.UseRouting();
    // Pay attention to the order
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

⑥ Configure Swagger

Configure services in ConfigureServices

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "SwaggerDemo", Version = "v1", Description = "Demo API for showing Swagger" });

    // The following two steps configure the "lock" on Swagger
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,  // Located in the Header
        Description = "Please enter the token directly here without adding 'Bearer' and then a space",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement{
        {
            new OpenApiSecurityScheme{
                Reference=new OpenApiReference{
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },Array.Empty<string>()
        }
    });

    // Display Swagger interface comments
    // Note: VS Code users need to manually configure the project's csproj file to generate comment XML files
    // See the PropertyGroup in the project file for details
    var fileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var filePath = Path.Combine(AppContext.BaseDirectory, fileName);
    c.IncludeXmlComments(filePath);
});

Use services in Configure

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        // Use Swagger middleware
        app.UseSwagger();
        // The "v1" here must match the one in c.SwaggerDoc("v1") above
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerDemo v1"));
    }

    app.UseHttpsRedirection();
    app.UseCors("any");
    app.UseRouting();
    // Pay attention to the order
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

⑦ Create UserController and Inject Login Services via Constructor

private readonly UserManager<AppUser> _userManger;  // User service
private readonly SignInManager<AppUser> _signInManger;  // Login service

private readonly RoleManager<IdentityRole> _roleManger; // Role service
private readonly JWTConfig _jwtConfig;  // Configuration framework injects configuration into entity class
public UserController(ILogger<UserController> logger, UserManager<AppUser> userManager,
        SignInManager<AppUser> signInManager, IOptions<JWTConfig> jwtConfig, RoleManager<IdentityRole> roleManger)
{
    this._logger = logger;
    this._userManger = userManager;
    this._signInManger = signInManager;
    this._jwtConfig = jwtConfig.Value;
    this._roleManger = roleManger;
}

Register User

/// <summary>
/// User Registration
/// AddAndUpdateUserrRegisterModel is a DTO to receive the object
/// AllowAnonymous: no permission verification required
/// Author: xxxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("RegisterUser")]
public async Task<Object> RegisterUser(AddAndUpdateUserrRegisterModel model)
{
    try
    {
        // Check if roles are included during registration
        if (model.Roles is null || model.Roles.Count <= 0)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "Role cannot be empty"));
        }
        // Loop to check if the roles the user registers for exist. Method for creating roles: AddRole()
        foreach (var item in model.Roles)
        {
            if (!await _roleManger.RoleExistsAsync(item))
            {
                return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "The role does not exist"));
            }
        }
        // Create a user object
        var user = new AppUser()
        {
            UserName = model.Email,
            FullName = model.FullName,
            Email = model.Email,
            DateCreated = DateTime.Now,
            DateModified = DateTime.UtcNow
        };
        // Register user
        var result = await _userManger.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            // After successful registration, get the temporarily created user
            var tempUser = await _userManger.FindByEmailAsync(model.Email);
            // Loop to assign roles to the created user
            foreach (var role in model.Roles)
            {
                await _userManger.AddToRoleAsync(tempUser, role); // Add role
            }
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "User successfully registered!", null));
        }
        // Return if user creation failed
        return await Task.FromResult(string.Join(",", result.Errors.Select(x => x.Description).ToArray()));
    }
    catch (System.Exception ex)
    {
        // Return exception
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, ex.Message, null));
    }
}

Login

/// <summary>
/// Generate Token
/// Author: xxxx
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
private string GenarateToken(AppUser user, List<string> roles)
{
    var jwtTokenHandle = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_jwtConfig.Key);

    // Configure Subject
    var claims = new List<Claim>()
    {
        new Claim(JwtRegisteredClaimNames.NameId,user.Id),
        new Claim(JwtRegisteredClaimNames.Email,user.Email),
        new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
    };
    foreach (var role in roles)
    {
        claims.Add(new Claim(ClaimTypes.Role,role));
    }
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        // Multiple roles
        Subject=new ClaimsIdentity(claims),

        // Single role
        // Subject = new ClaimsIdentity(new[]
        // {
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.NameId,user.Id),
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.Email,user.Email),
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
        //     // new System.Security.Claims.Claim(ClaimTypes.Role,"role")
        // }),

        // Expiration time: 12 hours
        Expires = DateTime.UtcNow.AddSeconds(6),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
        Audience = _jwtConfig.Audience,  // If not configured, it will return UnAuthorized
        Issuer = _jwtConfig.Issuer // Same as above
    };
    // Create token
    var token = jwtTokenHandle.CreateToken(tokenDescriptor);
    return jwtTokenHandle.WriteToken(token);
}
/// <summary>
/// User Login
/// LoginModel is a DTO for login
/// Author: xxxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
public async Task<object> Login(LoginModel model)
{
    try
    {
        if (!ModelState.IsValid)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "Invalid parameters!", null));
        }
        var result = await _signInManger.PasswordSignInAsync(model.UserName, model.Password, false, false);
        if (result.Succeeded)
        {
            // Get user on success
            var appUser = await _userManger.FindByNameAsync(model.UserName);
            var roles = (await _userManger.GetRolesAsync(appUser)).ToList();
            // await _userManger.GetRolesAsync(appUser);
            var user = new UserDto(appUser.FullName, appUser.Email, appUser.UserName, appUser.DateCreated, roles)
            {
                // Generate Token
                Token = GenarateToken(appUser,roles)
            };
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "Login successful", user));
        }
        else
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "Login failed", null));
        }

    }
    catch (System.Exception ex)
    {
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, ex.Message, null));
    }
}

Add Role

First, set the token obtained from the user login into Swagger, then access the interface restricted to the Admin role.

/// <summary>
/// Add Role
/// [Authorize(Roles ="Admin")] - Only users with the Admin role can access
/// Author: xxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[Authorize(Roles ="Admin")]
[HttpPost("AddRole")]
public async Task<object> AddRole(AddRoleModel model)
{
    try
    {
        if (model is null || string.IsNullOrWhiteSpace(model.Role))
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "Role cannot be empty"));
        }
        // Check if the role exists in the [AspNetRoles] table
        if (await _roleManger.RoleExistsAsync(model.Role))
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "Role already exists"));
        }
        var role = new IdentityRole()
        {
            Name = model.Role,
        };
        // Create role
        var result = await _roleManger.CreateAsync(role);
        if (result.Succeeded)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "Role created successfully!"));
        }
        else
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "Role creation failed!"));
        }

    }
    catch (System.Exception)
    {
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error));
    }

}

What if token permission validation fails? Here ASP.NET Core 5.0 introduces a new interface IAuthorizationMiddlewareResultHandler to handle permission validation. See the code below!

/// <summary>
/// This is a new authorization handling failure in ASP.NET Core 5, which can directly expose the request context, making it much easier!!!
/// Author: xxx
/// </summary>
public class AuthorizationHandleMiddleWare : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler authorizationHandleMiddleWare =new();
    public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        // When the token is invalid or does not exist, authorizeResult.Challenged is true
        if(authorizeResult.Challenged)
        {
            // TODO: After getting the user object from context, you can check the token here to distinguish whether the token is expired
            var a=context.Request.Headers["Authorization"];
            context.Response.StatusCode=(int)HttpStatusCode.OK;
            await context.Response.WriteAsJsonAsync(new ResponseModel(Enums.ResponseCode.UnAuthorized,"You are not authorized, please check if the Token is valid!"));
            return ;
        }
        // If token validation passes but the accessed resource has no permission, authorizeResult.Forbidden is true
        if(authorizeResult.Forbidden)
        {
            context.Response.StatusCode=(int)HttpStatusCode.OK;
            await context.Response.WriteAsJsonAsync(new ResponseModel(Enums.ResponseCode.ForBidden,"You do not have permission to access this!"));
            return ;
        }
        await authorizationHandleMiddleWare.HandleAsync(next,context,policy,authorizeResult);
    }
}

Additionally, you need to register the service in ConfigureServices:

// .NET 5 new permission validation middleware; inject dependency here, see AuthorizationHandleMiddleWare.cs for details
services.AddSingleton<IAuthorizationMiddlewareResultHandler,AuthorizationHandleMiddleWare>();

The above is a simple demo for login. For detailed code, please visit the repository: https://gitee.com/holyace/together/tree/JarryGu_develop/framework/JwtLoginDemo

Keep Exploring

Related Reading

More Articles
Same category / Same tag 6/22/2022

ASP.NET Core WebAPI Localization (Single Resource File)

Microsoft's default approach is one class corresponding to multiple resource files, which is cumbersome to use. This article introduces the use of a single resource file, where all classes in the entire project correspond to one set of multilingual resource files.

Continue Reading