Refactoring, database structure changed
This commit is contained in:
14
WatchIt.WebAPI/Services/Tokens/Configuration/JWT.cs
Normal file
14
WatchIt.WebAPI/Services/Tokens/Configuration/JWT.cs
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace WatchIt.WebAPI.Services.Tokens.Exceptions;
|
||||
|
||||
public class TokenNotExtendableException : Exception
|
||||
{
|
||||
public TokenNotExtendableException() : base() { }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace WatchIt.WebAPI.Services.Tokens.Exceptions;
|
||||
|
||||
public class TokenNotFoundException : Exception
|
||||
{
|
||||
public TokenNotFoundException() : base() { }
|
||||
}
|
||||
13
WatchIt.WebAPI/Services/Tokens/ITokensService.cs
Normal file
13
WatchIt.WebAPI/Services/Tokens/ITokensService.cs
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
160
WatchIt.WebAPI/Services/Tokens/TokensService.cs
Normal file
160
WatchIt.WebAPI/Services/Tokens/TokensService.cs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user