| | 769 | |
| | 770 | |
| | 771 | = Security measures |
| | 772 | == Authentication and Authorization |
| | 773 | === User Authentication with JWT |
| | 774 | ChapterX uses JSON Web Tokens (JWT) for authentication. The architecture is fully stateless so that means that no session is stored on the server between requests, so each request carries its own proof of identity in the token. After a successful login, the token is returned to the client in the JSON response body; the client is responsible for storing and attaching it to subsequent requests via the Authorization Bearer token header. |
| | 775 | Login flow: |
| | 776 | {{{ |
| | 777 | var user = await _userRepository.GetByEmailAsync(request.Email, cancellationToken) |
| | 778 | ?? throw new UnauthorizedAccessException("Invalid email or password."); |
| | 779 | |
| | 780 | if (!BCrypt.Net.BCrypt.Verify(request.Password, user.Password)) |
| | 781 | throw new UnauthorizedAccessException("Invalid email or password."); |
| | 782 | |
| | 783 | var token = _jwtTokenService.GenerateToken(user); |
| | 784 | return new LoginResponse(token, user.Id, user.Username, user.Email, user.Name, user.Surname, role); |
| | 785 | }}} |
| | 786 | The identical error message for both a missing user and a wrong password prevents email enumeration attacks, because an attacker cannot distinguish between the two failure modes. |
| | 787 | === Access Token |
| | 788 | The access token is generated by JwtTokenService. It is a signed, self-contained token that embeds the user's identity and role directly in its payload, eliminating the need for a database lookup on every authenticated request. |
| | 789 | Token generation: |
| | 790 | {{{ |
| | 791 | public string GenerateToken(User user) |
| | 792 | { |
| | 793 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!)); |
| | 794 | var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); |
| | 795 | |
| | 796 | var role = user.Admin != null ? "Admin" |
| | 797 | : user.Writer != null ? "Writer" |
| | 798 | : "RegularUser"; |
| | 799 | |
| | 800 | var claims = new[] |
| | 801 | { |
| | 802 | new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), // subject: user ID |
| | 803 | new Claim(JwtRegisteredClaimNames.Email, user.Email), // user email |
| | 804 | new Claim(JwtRegisteredClaimNames.UniqueName, user.Username), // username |
| | 805 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // unique token ID |
| | 806 | new Claim(ClaimTypes.Role, role) // role claim |
| | 807 | }; |
| | 808 | |
| | 809 | var token = new JwtSecurityToken( |
| | 810 | issuer: _configuration["Jwt:Issuer"], |
| | 811 | audience: _configuration["Jwt:Audience"], |
| | 812 | claims: claims, |
| | 813 | expires: DateTime.UtcNow.AddDays(7), |
| | 814 | signingCredentials: credentials |
| | 815 | ); |
| | 816 | |
| | 817 | return new JwtSecurityTokenHandler().WriteToken(token); |
| | 818 | } |
| | 819 | }}} |
| | 820 | The token is signed with HMAC-SHA256 using a symmetric key loaded from configuration. This ensures that any modification to the token payload after issuance will invalidate the signature, making it impossible for a client to forge or tamper with claims. The token expires after 7 days. |
| | 821 | === JWT Validation |
| | 822 | ASP.NET Core's built-in JwtBearer middleware intercepts every incoming HTTP request and validates the token before any controller action runs. |
| | 823 | {{{ |
| | 824 | builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) |
| | 825 | .AddJwtBearer(options => |
| | 826 | { |
| | 827 | options.TokenValidationParameters = new TokenValidationParameters |
| | 828 | { |
| | 829 | ValidateIssuer = true, |
| | 830 | ValidateAudience = true, |
| | 831 | ValidateLifetime = true, |
| | 832 | ValidateIssuerSigningKey = true, |
| | 833 | ValidIssuer = builder.Configuration["Jwt:Issuer"], |
| | 834 | ValidAudience = builder.Configuration["Jwt:Audience"], |
| | 835 | IssuerSigningKey = new SymmetricSecurityKey( |
| | 836 | Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) |
| | 837 | }; |
| | 838 | }); |
| | 839 | }}} |
| | 840 | === Security Filter Chain |
| | 841 | ASP.NET Core processes requests through an ordered middleware pipeline. Authentication and authorization are placed after CORS and exception handling, and before controller mapping, which means the token is validated on every request regardless of which controller handles it. |
| | 842 | === Role-Based Access Control |
| | 843 | The system defines three roles, derived at login time from the user's related entities in the database and embedded as a ClaimTypes.Role claim in the JWT. |
| | 844 | {{{ |
| | 845 | var role = user.Admin != null ? "Admin" |
| | 846 | : user.Writer != null ? "Writer" |
| | 847 | : "RegularUser"; |
| | 848 | }}} |