Merge branch 'main' of ssh://git.georesinwaybill.ru:222/espada/renis-backend

This commit is contained in:
ereshkigal 2024-09-07 15:14:58 +03:00
commit c39b481eb8
5 changed files with 250 additions and 8 deletions

View File

@ -13,4 +13,6 @@ public class User
public string Phone { get; set; } = null!; public string Phone { get; set; } = null!;
[Required] [Required]
public string Password { get; set; } = null!; public string Password { get; set; } = null!;
[Required]
public DateTime PasswordChangeDate { get; set; } = DateTime.UtcNow;
} }

View File

@ -1,5 +1,10 @@
using Serilog; using Serilog;
using Serilog.Exceptions; using Serilog.Exceptions;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -17,6 +22,28 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI(); app.UseSwaggerUI();
} }
// Authorization
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
string issuer = Environment.GetEnvironmentVariable("JWT_ISSUER") ?? "renis";
string audience = Environment.GetEnvironmentVariable("JWT_AUDIENCE") ?? "renis";
string secret = Environment.GetEnvironmentVariable("JWT_SECRET") ?? "TopSecretKeyForTheProtectionOfChocolateCookiesAndOtherSweetThings";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)),
ValidateIssuerSigningKey = true
};
});
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.Run(); app.Run();

View File

@ -0,0 +1,11 @@
using Renis.Database.Models;
namespace Renis.Services.Jwt;
public interface IJwtService
{
string GenerateAccessToken(User user);
string GenerateRefreshToken(User user);
Tuple<bool, string> ValidateAccessToken(string? token);
Task<Tuple<bool, string>> ValidateRefreshToken(string? token);
}

201
Services/Jwt/JwtService.cs Normal file
View File

@ -0,0 +1,201 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Renis.Database.Models;
using Microsoft.IdentityModel.Tokens;
using Renis.Repositories;
namespace Renis.Services.Jwt;
public class JwtService : IJwtService
{
private readonly string _secretKey;
private readonly string _issuer;
private readonly string _audience;
private readonly IUserRepository _userRepo;
private readonly ILogger<JwtService> _logger;
public JwtService(ILogger<JwtService> logger, IUserRepository userRepo)
{
_logger = logger;
_userRepo = userRepo;
// TODO: Change issuer & audience
_issuer = Environment.GetEnvironmentVariable("AUTH_JWT_ISSUER") ?? "renis";
_audience = Environment.GetEnvironmentVariable("AUTH_JWT_AUDIENCE") ?? "renis";
_secretKey = Environment.GetEnvironmentVariable("AUTH_JWT_SECRET")!;
if (_secretKey == null)
{
throw new Exception("Missing AUTH_JWT_SECRET environment variable");
}
}
/// <summary>
/// Generates an access token for the specified user.
/// </summary>
/// <param name="user">The user for whom the access token is generated.</param>
/// <returns>The generated access token as a string.</returns>
public string GenerateAccessToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Convert.FromBase64String(_secretKey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = _issuer,
Audience = _audience,
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.Phone),
new Claim(ClaimsIdentity.DefaultRoleClaimType,"User"),
new Claim(ClaimTypes.AuthenticationMethod, "Access")
}),
Expires = DateTime.UtcNow.AddMinutes(50),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
/// <summary>
/// Generates a refresh token for the specified user.
/// </summary>
/// <param name="user">The user for whom the refresh token is generated.</param>
/// <returns>The generated refresh token as a string.</returns>
public string GenerateRefreshToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Convert.FromBase64String(_secretKey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = _issuer,
Audience = _audience,
Subject = new ClaimsIdentity(new[]
{
new Claim("PasswordChangeDate", user.PasswordChangeDate.ToString()),
new Claim(ClaimTypes.Name, user.Phone),
new Claim(ClaimsIdentity.DefaultRoleClaimType,"User"),
new Claim(ClaimTypes.AuthenticationMethod, "Refresh")
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
/// <summary>
/// Validates the access token and extracts the username from it if valid.
/// Returns a tuple indicating whether the token is valid and the extracted username.
/// </summary>
/// <param name="token">The access token to validate.</param>
/// <returns>A tuple indicating whether the token is valid and the extracted username.</returns>
public Tuple<bool, string> ValidateAccessToken(string? token)
{
try
{
if (token == null)
{
_logger.LogWarning("JwtService: No access token string provided");
return new (false, "");
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = Convert.FromBase64String(_secretKey);
TokenValidationParameters validationParameters = new() {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = _issuer,
ValidateAudience = true,
ValidAudience = _audience,
ClockSkew = TimeSpan.Zero
};
// Валидация токена
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
JwtSecurityToken validatedJwt = (JwtSecurityToken)validatedToken;
var username = validatedJwt.Claims.First(claim => claim.Type == "unique_name").Value;
// Проверка типа токена
if (validatedJwt.Claims.First(claim => claim.Type == ClaimTypes.AuthenticationMethod).Value != "Access")
{
_logger.LogWarning("JwtService: the token was not a access token");
return new (false, "");
}
return new (true, username);
}
catch (Exception e)
{
_logger.LogWarning("JwtService: Token invalidated due to exception:\n{Exception}", e);
return new (false, "");;
}
}
/// <summary>
/// Validates the refresh token and extracts the username from it if valid.
/// Returns a tuple indicating whether the token is valid and the extracted username.
/// </summary>
/// <param name="token">The refresh token to validate.</param>
/// <returns>A tuple indicating whether the token is valid and the extracted username.</returns>
public async Task<Tuple<bool, string>> ValidateRefreshToken(string? token)
{
try
{
if (token == null)
{
_logger.LogWarning("JwtService: No refresh token string provided");
return new (false, "");;
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = Convert.FromBase64String(_secretKey);
TokenValidationParameters validationParameters = new() {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = _issuer,
ValidateAudience = true,
ValidAudience = _audience,
ClockSkew = TimeSpan.Zero
};
// Валидация токена
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
JwtSecurityToken validatedJwt = (JwtSecurityToken)validatedToken;
var passwordChangeDate = validatedJwt.Claims.First(claim => claim.Type == "PasswordChangeDate").Value;
var username = validatedJwt.Claims.First(claim => claim.Type == "unique_name").Value;
// Проверка типа токена
if (validatedJwt.Claims.First(claim => claim.Type == "authmethod").Value != "Refresh")
{
_logger.LogWarning("JwtService: the token was not a refresh token");
return new (false, "");
}
var user = await _userRepo.GetUserByPhone(username);
if (user == null)
{
return new (false, "");
}
// Проверка, что дата изменения пароля совпадает с фактической
if (user.PasswordChangeDate.ToString() != passwordChangeDate)
{
username=null;
_logger.LogWarning("JwtService: the password change date was not equal to the one in the token\n1. {1}\n2. {2}", passwordChangeDate, user.PasswordChangeDate.ToString());
return new (false, "");
}
return new (true, username);
}
catch (Exception e)
{
_logger.LogWarning("JwtService: Token invalidated due to exception:\n{Exception}", e);
return new (false, "");
}
}
}

View File

@ -14,16 +14,17 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
<PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0" /> <PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" /> <PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Formatting.OpenSearch" Version="1.0.0" /> <PackageReference Include="Serilog.Formatting.OpenSearch" Version="1.2.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" /> <PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.OpenSearch" Version="1.0.0" /> <PackageReference Include="Serilog.Sinks.OpenSearch" Version="1.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
</ItemGroup> </ItemGroup>
</Project> </Project>