OnlineSalesAutoCrop/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Auth/RefreshTokenService.cs

409 lines
13 KiB
C#
Raw Normal View History

2026-06-16 17:22:46 +06:00
using Ease.NetCore.DataAccess;
2026-06-15 18:26:58 +06:00
using Ease.NetCore.DataAccess.SQL;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using OnlineSalesAutoCrop.CoreAPI.Models.Global;
using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems;
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Auth;
using System;
using System.Data;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.Auth;
public class RefreshTokenService : IRefreshTokenService
{
private readonly AppSettings _settings;
public RefreshTokenService(IOptions<AppSettings> settings)
{
_settings = settings.Value;
}
public async Task<bool> AddAsync(InsertRefreshTokenRequest refreshToken)
{
bool returnValue = false;
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
AddAsync(tc, refreshToken);
2026-06-15 18:26:58 +06:00
tc.End();
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return returnValue;
}
private bool AddAsync( TransactionContext tc, InsertRefreshTokenRequest refreshToken)
2026-06-15 18:26:58 +06:00
{
bool returnValue = false;
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.VarChar, pValue: refreshToken.UserId),
SqlHelperExtension.CreateInParam(pName: "@TokenHash", pType: SqlDbType.VarChar, pValue: refreshToken.TokenHash),
SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: refreshToken.IpAddress),
SqlHelperExtension.CreateInParam(pName: "@CreatedAt", pType: SqlDbType.DateTime, pValue: DateTime.Now),
SqlHelperExtension.CreateInParam(pName: "@ExpiredAt", pType: SqlDbType.DateTime, pValue: DateTime.Now.AddMinutes(_settings.RefreshTokenDuration)),
];
_ = tc.ExecuteNonQuerySp(spName: "dbo.InsertRefreshToken", parameterValues: p);
2026-06-15 18:26:58 +06:00
returnValue = true;
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return returnValue;
}
public async Task<RefreshTokenResponse> GetByTokenHashAsync(string tokenHash)
{
RefreshTokenResponse response = new();
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode);
try
{
2026-06-21 16:51:27 +06:00
response= await GetByTokenHashAsync(tc, tokenHash);
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return response;
}
public async Task<UserByRefreshTokenResponse> GetUserByRefreshTokenAsync(string refreshToken)
{
UserByRefreshTokenResponse response = new();
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode);
try
{
response = await GetUserByRefreshTokenAsync(tc, refreshToken);
2026-06-15 18:26:58 +06:00
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return response;
}
private async Task<RefreshTokenResponse> GetByTokenHashAsync(TransactionContext tc, string tokenHash)
{
RefreshTokenResponse response = new();
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.VarChar, pValue: tokenHash),
];
using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetRefreshTokenByTokenHash", parameterValues: p))
{
if (dr.Read())
{
2026-06-21 16:51:27 +06:00
response.UserId = dr.GetInt32(0);
response.TokenHash = dr.GetString(1);
response.IpAddress = dr.GetString(2);
response.ExpiredAt = dr.GetDateTime(3);
response.RevokedAt = dr.IsDBNull(4) ? null : dr.GetDateTime(4);
}
dr.Close();
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return response;
}
2026-06-21 16:51:27 +06:00
private async Task<UserByRefreshTokenResponse> GetUserByRefreshTokenAsync(TransactionContext tc, string refreshToken)
{
UserByRefreshTokenResponse response = new();
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.VarChar, pValue: refreshToken),
];
using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetUserByRefreshToken", parameterValues: p))
{
if (dr.Read())
{
response.UserId = dr.GetInt32(0);
response.LoginId = dr.GetString(1);
response.EmailAddress = dr.IsDBNull(2) ? string.Empty : dr.GetString(2);
response.AuthKey = dr.IsDBNull(3) ? string.Empty : dr.GetString(3);
response.IsActive = dr.IsDBNull(4) ? true : false;
}
dr.Close();
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return response;
}
2026-06-15 18:26:58 +06:00
public async Task<bool> RevokeAllForUserAsync(int userId)
{
bool returnValue = false;
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: userId)
];
_ = await tc.ExecuteNonQuerySpAsync(spName: "dbo.RevokedAllRefreshToken", parameterValues: p);
returnValue = true;
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return returnValue;
}
public async Task<bool> RevokeAsync(RevokedRefreshTokenRequest token)
{
bool returnValue = false;
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
try
{
returnValue = RevokeAsync(tc, token);
2026-06-15 18:26:58 +06:00
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return returnValue;
}
private bool RevokeAsync(TransactionContext tc, RevokedRefreshTokenRequest token)
{
bool returnValue = false;
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.NVarChar, pValue: token.RefreshToken)
];
_ = tc.ExecuteNonQuerySp(spName: "dbo.RevokedAllRefreshToken", parameterValues: p);
returnValue = true;
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return returnValue;
}
2026-06-21 16:51:27 +06:00
private bool RevokeRefreshTokenByUserIdAsync(TransactionContext tc, int userId)
{
bool returnValue = false;
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: userId)
];
_ = tc.ExecuteNonQuerySp(spName: "dbo.RevokedAllRefreshTokenByUserId", parameterValues: p);
returnValue = true;
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return returnValue;
}
public async Task<GenerateRefreshTokenResponse> GenerateRefreshTokenByUserAsync(GenerateRefreshTokenRequest request)
2026-06-15 18:26:58 +06:00
{
GenerateRefreshTokenResponse refreshTokenResponse = new();
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
try
{
2026-06-21 16:51:27 +06:00
// Rotate: revoke old token, issue new one
RevokeRefreshTokenByUserIdAsync(tc, request.UserId);
2026-06-21 16:51:27 +06:00
refreshTokenResponse= await IssueTokensAsync(tc,request.UserId, request.IpAddress);
2026-06-21 16:51:27 +06:00
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
2026-06-21 16:51:27 +06:00
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
2026-06-21 16:51:27 +06:00
return refreshTokenResponse;
}
public async Task<string> GenerateRefreshTokenAsync(string refreshToken, string ipAddress)
{
string refreshTokenResponse = string.Empty;
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
try
{
bool isValid = IsValidRefreshTokenAsync(tc, refreshToken);
if(string.IsNullOrEmpty(refreshToken) && !isValid)
{
throw new Exception($"Refresh token has expired or been revoked- {refreshToken}");
}
var storedToken = await GetByTokenHashAsync(tc, refreshToken);
RevokeRefreshTokenByUserIdAsync(tc, storedToken.UserId);
var result = await IssueTokensAsync(tc, storedToken.UserId, ipAddress);
refreshTokenResponse = result.RefreshToken;
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
return refreshTokenResponse;
2026-06-15 18:26:58 +06:00
}
// ----- private helpers -----
2026-06-21 16:51:27 +06:00
private bool IsValidRefreshTokenAsync(TransactionContext tc, string refreshToken )
{
try
{
string sql = SQLParser.MakeSQL("Select Count(1) FROM RefreshTokens where TokenHash = %s AND RevokedAt is null", refreshToken);
var returnValue = tc.ExecuteScalar(sql);
return Convert.ToInt32(returnValue) > 0 ? true : false;
}
catch(Exception)
{
throw;
}
}
private async Task<GenerateRefreshTokenResponse> IssueTokensAsync(TransactionContext tc, int userId, string ipAddress)
2026-06-15 18:26:58 +06:00
{
string rawRefreshToken = GenerateRowToken();
2026-06-21 16:51:27 +06:00
string tokenHash = HashToken(rawRefreshToken);
2026-06-15 18:26:58 +06:00
var refreshToken = new InsertRefreshTokenRequest
{
2026-06-21 16:51:27 +06:00
UserId = userId,
TokenHash = tokenHash,
2026-06-15 18:26:58 +06:00
IpAddress = ipAddress,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddDays(_settings.RefreshTokenDuration)
};
AddAsync(tc,refreshToken);
2026-06-15 18:26:58 +06:00
return new GenerateRefreshTokenResponse
2026-06-15 18:26:58 +06:00
{
2026-06-21 16:51:27 +06:00
RefreshToken = tokenHash,
ExpireTime = DateTime.UtcNow.AddMinutes(_settings.RefreshTokenDuration)
2026-06-15 18:26:58 +06:00
};
}
private static string HashToken(string token)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
return Convert.ToBase64String(bytes);
}
private string GenerateRowToken()
{
var bytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
}