.NET 6 - Authentication and Authorization with JWT

文章目錄

In this article, we will use .NET 6 to authenticate and authorize user with JWT in role based.

Design

  1. Authenticate user and generate JWT.
  2. Pass JWT in each request.
  3. Validate JWT.
  4. Authorize

Implementaion

1. Models

  • User
    public class User
    {
        public int UserId { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public UserRole Role { get; set; }
        public string? Token { get; set; }
    }
    
  • UserRole
    public enum UserRole { Admin, User }
    

2. Fakedata

  • Package required
    • BCrypt.Net-Next BCrypt.Net-Next package
  • Encrypt user’s password by HashPassword method in BCrypt.
// UserService.cs
private List<User> _users = new List<User>
{
    new User { UserId = 1, UserName = "admin", Password = BCrypt.Net.BCrypt.HashPassword("admin"), Role = UserRole.Admin },
    new User { UserId = 2, UserName = "test", Password = BCrypt.Net.BCrypt.HashPassword("test"), Role = UserRole.User }
};

3. Authenticate

  • Verify username and password
    • Use Verify method in Bcrypt to verify encrypted password.
    • If verfify success, then generate JWT token.
// UserService.cs
public User Authenticate(User model)
{
    User user = _users.SingleOrDefault(m => m.UserName == model.UserName);

    if(user == null || !BCrypt.Net.BCrypt.Verify(model.Password, user.Password))
        throw new Exception("Username or password is incorrect");

    user.Token = _jwtUtils.GenerateJwtToken(user);
    return user;
}
  • Generate JWT token
    • Package required
      • Microsoft.IdentityModel.Tokens
      • System.IdentityModel.Tokens Microsoft.IdentityModel.Tokens package
// JwtUtil.cs
public string GenerateJwtToken(User user)
{
    // generate token that is valid for 7 days
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_config["JWT:SignKey"]);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[] { new Claim("id", user.UserId.ToString()) }),
        Expires = DateTime.UtcNow.AddDays(7),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}

4. Request with JWT token in Header

  • Test with swagger, config security in Swagger generator.
// Program.cs
builder.Services.AddSwaggerGen(options => {
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme."
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] {}
        }
    });
});

set Authorization header in swagger set Bearer token in swagger

5. Validate JWT token

  • Add Middleware for each of the requests to check whether token is valid before executing controller.
// Program.cs
app.UseMiddleware<JwtMiddleware>();
  • If token is valid, memorize the user in HttpContext for authorization.
// JwtMiddleware.cs
private readonly RequestDelegate _next;

public JwtMiddleware(RequestDelegate next)
{
    _next = next;
}

public async Task InvokeAsync(HttpContext context, UserService userService, JwtUtil jwtUtil)
{
    var token = context.Request.Headers["Authorization"].FirstOrDefault();
    var userId = jwtUtil.ValidateJwtToken(token);
    if (userId != null)
    {
        // attach user to context on successful jwt validation
        context.Items["User"] = userService.GetUser(userId.Value);
    }

    await _next(context);
}
  • Verify token by ValidateToken method in JwtSecurityTokenHandler
// JwtUtil.cs
public int? ValidateJwtToken(string token)
{
    if (token == null)
        return null;

    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_config["JWT:SignKey"]);
    try
    {
        tokenHandler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false,
            // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
            ClockSkew = TimeSpan.Zero
        }, out SecurityToken validatedToken);

        var jwtToken = (JwtSecurityToken)validatedToken;
        var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

        // return user id from JWT token if validation successful
        return userId;
    }
    catch
    {
        // return null if validation fails
        return null;
    }
}

6. Authorization

  • Customize Attribute
    • AllowAnonymousAttribute
    [AttributeUsage(AttributeTargets.Method)]
    public class AllowAnonymousAttribute : Attribute {}
    
    • AuthorizeAttribute
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AuthorizeAttribute : Attribute, IAuthorizationFilter
    {
        private readonly IList<UserRole> _roles;
    
        public AuthorizeAttribute(params UserRole[] roles)
        {
            _roles = roles ?? new UserRole[] { };
        }
    
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            // skip authorization if action is decorated with [AllowAnonymous] attribute
            var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
            if (allowAnonymous)
                return;
    
            // authorization
            var user = (User)context.HttpContext.Items["User"];
            if (user == null || (_roles.Any() && !_roles.Contains(user.Role)))
            {
                // not logged in or role not authorized
                context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
            }
        }
    }
    

7. Controller

  • Before execute the method, use attibute properties that we customized, such as AllowAnonymous and Authorize to check whether the user can access or not.
// UserController.cs
[AllowAnonymous]
[HttpPost]
public User Login(User user)
{
    return _userService.Authenticate(user);
}

[Authorize(UserRole.Admin)]
[HttpGet]
public List<User> GetAllUsers()
{
    return _userService.GetAllUsers();
}

[HttpGet("{id}")]
public User GetUser(int id)
{
    return _userService.GetUser(id);
}

Reference