Refactoring, database structure changed

This commit is contained in:
2025-03-03 00:56:32 +01:00
Unverified
parent d3805ef3db
commit c603c41c0b
913 changed files with 21764 additions and 32775 deletions

View File

@@ -0,0 +1,14 @@
namespace WatchIt.WebAPI.Services.Tokens.Configuration;
public class JWT
{
#region PROPERTIES
public string Key { get; set; } = null!;
public string Issuer { get; set; } = null!;
public string Audience { get; set; } = null!;
public string Algorithm { get; set; } = null!;
public TokensLifetime Lifetime { get; set; } = null!;
#endregion
}

View File

@@ -0,0 +1,11 @@
namespace WatchIt.WebAPI.Services.Tokens.Configuration;
public class TokenLifetime
{
#region PROPERTIES
public int Normal { get; set; }
public int? Extended { get; set; }
#endregion
}

View File

@@ -0,0 +1,11 @@
namespace WatchIt.WebAPI.Services.Tokens.Configuration;
public class TokensLifetime
{
#region PROPERTIES
public TokenLifetime AccessToken { get; set; } = null!;
public TokenLifetime RefreshToken { get; set; } = null!;
#endregion
}

View File

@@ -0,0 +1,6 @@
namespace WatchIt.WebAPI.Services.Tokens.Exceptions;
public class TokenNotExtendableException : Exception
{
public TokenNotExtendableException() : base() { }
}

View File

@@ -0,0 +1,6 @@
namespace WatchIt.WebAPI.Services.Tokens.Exceptions;
public class TokenNotFoundException : Exception
{
public TokenNotFoundException() : base() { }
}

View File

@@ -0,0 +1,13 @@
using WatchIt.Database.Model.Accounts;
namespace WatchIt.WebAPI.Services.Tokens;
public interface ITokensService
{
string CreateAccessToken(Account account);
Task<string> CreateRefreshTokenAsync(Account account, bool isExtendable);
Task<Account> ExtendRefreshTokenAsync(string refreshToken, string accessToken);
Task RevokeRefreshTokenAsync(string stringToken);
Task RevokeRefreshTokenAsync(Guid token);
Task RevokeAccountRefreshTokensAsync(Account account);
}

View File

@@ -0,0 +1,21 @@
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
namespace WatchIt.WebAPI.Services.Tokens;
public static class SecurityTokenDescriptorExtensions
{
#region PUBLIC METHODS
public static string ToJwtString(this SecurityTokenDescriptor tokenDescriptor)
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
handler.InboundClaimTypeMap.Clear();
SecurityToken token = handler.CreateToken(tokenDescriptor);
return handler.WriteToken(token);
}
#endregion
}

View File

@@ -0,0 +1,160 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using WatchIt.Database.Model.Accounts;
using WatchIt.DTO.Models.Controllers.Accounts;
using WatchIt.WebAPI.Constants;
using WatchIt.WebAPI.Repositories.Accounts;
using WatchIt.WebAPI.Services.Tokens.Configuration;
using WatchIt.WebAPI.Services.Tokens.Exceptions;
namespace WatchIt.WebAPI.Services.Tokens;
public class TokensService : ITokensService
{
#region SERVICES
private readonly JWT _configuration;
private readonly IAccountsRepository _accountsRepository;
#endregion
#region CONSTRUCTORS
public TokensService(IConfiguration configuration, IAccountsRepository accountsRepository)
{
_configuration = configuration.GetSection("Authentication").GetSection("JWT").Get<JWT>()!;
_accountsRepository = accountsRepository;
}
#endregion
#region PUBLIC METHODS
public string CreateAccessToken(Account account)
{
int lifetime = _configuration.Lifetime.AccessToken.Normal;
DateTimeOffset expirationDate = new DateTimeOffset(DateTime.UtcNow.AddMinutes(lifetime));
SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(
[
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Sub, account.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Exp, expirationDate.Ticks.ToString()),
new Claim(AdditionalClaimNames.Admin, account.IsAdmin.ToString())
]),
Issuer = _configuration.Issuer,
Audience = _configuration.Audience,
Expires = expirationDate.UtcDateTime,
SigningCredentials = new SigningCredentials(CreateSecurityKey(), _configuration.Algorithm),
};
return descriptor.ToJwtString();
}
public async Task<string> CreateRefreshTokenAsync(Account account, bool isExtendable)
{
Guid newToken = Guid.NewGuid();
DateTimeOffset expirationDate = GetExpirationDate(_configuration.Lifetime.RefreshToken, isExtendable);
AccountRefreshToken tokenEntity = AccountsMappers.CreateAccountRefreshTokenEntity(newToken, account.Id, expirationDate, isExtendable);
await _accountsRepository.AddRefreshTokenAsync(tokenEntity);
return Convert.ToBase64String(newToken.ToByteArray());
}
public async Task<Account> ExtendRefreshTokenAsync(string refreshToken, string accessToken)
{
long accountId = ValidateExpiredAccessTokenAndGetAccountId(accessToken);
Account? account = await _accountsRepository.GetAsync(accountId, x => x.Include(y => y.RefreshTokens));
if (account is null)
{
throw new SecurityTokenException("Invalid token");
}
Guid token = new Guid(Convert.FromBase64String(refreshToken));
AccountRefreshToken? tokenEntity = account.RefreshTokens.FirstOrDefault(x => x.Token == token);
if (tokenEntity is null)
{
throw new SecurityTokenException("Invalid token");
}
if (tokenEntity.ExpirationDate < DateTimeOffset.Now)
{
throw new SecurityTokenExpiredException();
}
DateTimeOffset expirationDate = GetExpirationDate(_configuration.Lifetime.RefreshToken, tokenEntity.IsExtendable);
await _accountsRepository.UpdateRefreshTokenAsync(tokenEntity, x => x.UpdateExpirationDate(expirationDate));
return account;
}
public async Task RevokeRefreshTokenAsync(string stringToken) =>
await RevokeRefreshTokenAsync(new Guid(Convert.FromBase64String(stringToken)));
public async Task RevokeRefreshTokenAsync(Guid token) =>
await _accountsRepository.DeleteRefreshTokenAsync(token);
public async Task RevokeAccountRefreshTokensAsync(Account account) =>
await _accountsRepository.DeleteUserRefreshTokensAsync(account.Id);
#endregion
#region PRIVATE METHODS
private long ValidateExpiredAccessTokenAndGetAccountId(string accessToken)
{
TokenValidationParameters tokenValidation = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = false,
ValidIssuer = _configuration.Issuer,
ValidAudience = _configuration.Audience,
IssuerSigningKey = CreateSecurityKey(),
ClockSkew = TimeSpan.FromMinutes(1),
};
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(accessToken, tokenValidation, out SecurityToken validatedToken);
JwtSecurityToken? jwtSecurityToken = validatedToken as JwtSecurityToken;
if (jwtSecurityToken is null || !jwtSecurityToken.Header.Alg.Equals(_configuration.Algorithm, StringComparison.InvariantCultureIgnoreCase))
{
throw new SecurityTokenException("Invalid token");
}
Claim? sub = jwtSecurityToken.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Sub);
if (sub is null || !long.TryParse(sub.Value, out long accountId))
{
throw new SecurityTokenException("Invalid token");
}
return accountId;
}
private SymmetricSecurityKey CreateSecurityKey()
{
string stringKey = _configuration.Key;
byte[] encodedKey = Encoding.UTF8.GetBytes(stringKey);
SymmetricSecurityKey securityKey = new SymmetricSecurityKey(encodedKey);
return securityKey;
}
private DateTimeOffset GetExpirationDate(TokenLifetime tokenConfiguration, bool isExtendable = false)
{
int lifetime = isExtendable ? tokenConfiguration.Extended ?? tokenConfiguration.Normal : tokenConfiguration.Normal;
DateTimeOffset expirationDate = DateTimeOffset.UtcNow.AddMinutes(lifetime);
return expirationDate;
}
#endregion
}

View File

@@ -0,0 +1,8 @@
using WatchIt.Database.Model.Accounts;
namespace WatchIt.WebAPI.Services.User;
public interface IUserService
{
Task<Account> GetAccountAsync();
}

View File

@@ -0,0 +1,80 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using WatchIt.Database.Model.Accounts;
using WatchIt.WebAPI.Constants;
using WatchIt.WebAPI.Repositories.Accounts;
using WatchIt.WebAPI.Services.Tokens;
namespace WatchIt.WebAPI.Services.User;
public class UserService : IUserService
{
#region SERVICES
private readonly IHttpContextAccessor _accessor;
private readonly IAccountsRepository _accountsRepository;
#endregion
#region CONSTRUCTORS
public UserService(IHttpContextAccessor accessor, IAccountsRepository accountsRepository)
{
_accessor = accessor;
_accountsRepository = accountsRepository;
}
#endregion
#region PUBLIC METHODS
public async Task<Account> GetAccountAsync()
{
long? id = GetAccountId();
if (!id.HasValue)
{
throw new SecurityTokenException("Incorrect sub claim");
}
Account? account = await _accountsRepository.GetAsync(id.Value);
if (account is null)
{
throw new SecurityTokenException("Account with sub claim id not found");
}
return account;
}
#endregion
#region PRIVATE METHODS
private ClaimsPrincipal? GetClaims()
{
if (_accessor.HttpContext is null)
{
throw new NullReferenceException();
}
return _accessor.HttpContext.User;
}
private long? GetAccountId()
{
ClaimsPrincipal? user = GetClaims();
Claim? subClaim = user?.FindFirst(JwtRegisteredClaimNames.Sub);
if (subClaim is null)
{
return null;
}
long.TryParse(subClaim.Value, out long accountId);
return accountId;
}
#endregion
}