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;
|
2026-07-06 17:23:05 +06:00
|
|
|
|
using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems;
|
2026-06-15 18:26:58 +06:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
public async Task<UserByRefreshTokenResponse> GetUserByRefreshTokenAsync(string refreshToken)
|
2026-06-15 18:26:58 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
UserByRefreshTokenResponse response = new();
|
2026-06-15 18:26:58 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
response = await GetUserByRefreshTokenAsync(tc, refreshToken);
|
2026-06-21 16:51:27 +06:00
|
|
|
|
tc.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ie)
|
|
|
|
|
|
{
|
|
|
|
|
|
tc?.HandleError();
|
|
|
|
|
|
|
|
|
|
|
|
throw DBCustomError.GenerateCustomError(ie);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException(e.Message, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
public async Task<GenerateRefreshTokenResponse> GenerateRefreshTokenByUserAsync(GenerateRefreshTokenRequest request)
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
GenerateRefreshTokenResponse refreshTokenResponse = new();
|
2026-06-21 16:51:27 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
|
2026-06-21 16:51:27 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
// Rotate: revoke old token, issue new one
|
|
|
|
|
|
RevokeRefreshTokenByUserIdAsync(tc, request.UserId);
|
|
|
|
|
|
|
|
|
|
|
|
refreshTokenResponse= await IssueTokensAsync(tc,request.UserId, request.IpAddress);
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
return refreshTokenResponse;
|
2026-06-16 11:36:11 +06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
public async Task<string> GenerateRefreshTokenAsync(string refreshToken, string ipAddress)
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
string refreshTokenResponse = string.Empty;
|
2026-06-21 16:51:27 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
|
|
|
|
|
|
try
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
bool isValid = IsValidRefreshTokenAsync(tc, refreshToken);
|
|
|
|
|
|
if(string.IsNullOrEmpty(refreshToken) && !isValid)
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
throw new Exception($"Refresh token has expired or been revoked- {refreshToken}");
|
2026-06-21 16:51:27 +06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
var storedToken = await GetByTokenHashAsync(tc, refreshToken);
|
2026-06-21 16:51:27 +06:00
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
RevokeRefreshTokenByUserIdAsync(tc, storedToken.UserId);
|
2026-06-15 18:26:58 +06:00
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
var result = await IssueTokensAsync(tc, storedToken.UserId, ipAddress);
|
|
|
|
|
|
refreshTokenResponse = result.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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
return refreshTokenResponse;
|
2026-06-15 18:26:58 +06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 17:23:05 +06:00
|
|
|
|
public async Task<bool> InvalidRefreshTokenAsync( TransactionContext tc, int userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool response = false;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
RevokeRefreshTokenByUserIdAsync(tc, userId);
|
|
|
|
|
|
response = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException(e.Message, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
#region Private Methods
|
|
|
|
|
|
// ----- private helpers -----
|
|
|
|
|
|
|
|
|
|
|
|
private bool IsValidRefreshTokenAsync(TransactionContext tc, string refreshToken )
|
2026-06-15 18:26:58 +06:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
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;
|
2026-06-15 18:26:58 +06:00
|
|
|
|
}
|
2026-06-22 11:53:49 +06:00
|
|
|
|
catch(Exception)
|
2026-06-15 18:26:58 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
throw;
|
2026-06-15 18:26:58 +06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
private bool AddAsync(TransactionContext tc, InsertRefreshTokenRequest refreshToken)
|
2026-06-16 11:36:11 +06:00
|
|
|
|
{
|
|
|
|
|
|
bool returnValue = false;
|
2026-06-22 11:53:49 +06:00
|
|
|
|
|
2026-06-16 11:36:11 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
SqlParameter[] p =
|
2026-06-22 11:53:49 +06:00
|
|
|
|
[
|
|
|
|
|
|
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),
|
2026-07-06 17:23:05 +06:00
|
|
|
|
SqlHelperExtension.CreateInParam(pName: "@ExpiredAt", pType: SqlDbType.DateTime, pValue: refreshToken.ExpiresAt),
|
2026-06-22 11:53:49 +06:00
|
|
|
|
];
|
|
|
|
|
|
_ = tc.ExecuteNonQuerySp(spName: "dbo.InsertRefreshToken", parameterValues: p);
|
2026-06-16 11:36:11 +06:00
|
|
|
|
|
|
|
|
|
|
returnValue = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException(e.Message, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return returnValue;
|
|
|
|
|
|
}
|
2026-06-21 16:51:27 +06:00
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
private async Task<RefreshTokenResponse> GetByTokenHashAsync(TransactionContext tc, string tokenHash)
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
RefreshTokenResponse response = new();
|
2026-06-21 16:51:27 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
SqlParameter[] p =
|
2026-06-22 11:53:49 +06:00
|
|
|
|
[
|
|
|
|
|
|
SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.VarChar, pValue: tokenHash),
|
|
|
|
|
|
];
|
|
|
|
|
|
using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetRefreshTokenByTokenHash", parameterValues: p))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (dr.Read())
|
|
|
|
|
|
{
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-06-21 16:51:27 +06:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException(e.Message, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
return response;
|
2026-06-21 16:51:27 +06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
private bool RevokeRefreshTokenByUserIdAsync(TransactionContext tc, int userId)
|
2026-06-15 18:26:58 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
bool returnValue = false;
|
2026-06-16 11:36:11 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
SqlParameter[] p =
|
|
|
|
|
|
[
|
|
|
|
|
|
SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: userId)
|
|
|
|
|
|
];
|
|
|
|
|
|
_ = tc.ExecuteNonQuerySp(spName: "dbo.RevokedAllRefreshTokenByUserId", parameterValues: p);
|
2026-06-16 11:36:11 +06:00
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
returnValue = true;
|
2026-06-16 11:36:11 +06:00
|
|
|
|
|
2026-06-21 16:51:27 +06:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException(e.Message, e);
|
|
|
|
|
|
}
|
2026-06-16 11:36:11 +06:00
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
return returnValue;
|
2026-06-21 16:51:27 +06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
private async Task<UserByRefreshTokenResponse> GetUserByRefreshTokenAsync(TransactionContext tc, string refreshToken)
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
UserByRefreshTokenResponse response = new();
|
2026-06-21 16:51:27 +06:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
SqlParameter[] p =
|
|
|
|
|
|
[
|
|
|
|
|
|
SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.VarChar, pValue: refreshToken),
|
|
|
|
|
|
];
|
|
|
|
|
|
using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetUserByRefreshToken", parameterValues: p))
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
if (dr.Read())
|
2026-06-21 16:51:27 +06:00
|
|
|
|
{
|
2026-06-22 11:53:49 +06:00
|
|
|
|
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;
|
2026-06-21 16:51:27 +06:00
|
|
|
|
}
|
2026-06-22 11:53:49 +06:00
|
|
|
|
dr.Close();
|
2026-06-16 11:36:11 +06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException(e.Message, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
return response;
|
2026-06-21 16:51:27 +06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<GenerateRefreshTokenResponse> IssueTokensAsync(TransactionContext tc, int userId, string ipAddress)
|
2026-06-15 18:26:58 +06:00
|
|
|
|
{
|
2026-06-16 11:36:11 +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)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-16 15:18:22 +06:00
|
|
|
|
AddAsync(tc,refreshToken);
|
2026-06-15 18:26:58 +06:00
|
|
|
|
|
2026-06-16 11:36:11 +06:00
|
|
|
|
return new GenerateRefreshTokenResponse
|
2026-06-15 18:26:58 +06:00
|
|
|
|
{
|
2026-06-21 16:51:27 +06:00
|
|
|
|
RefreshToken = tokenHash,
|
2026-07-06 17:23:05 +06:00
|
|
|
|
ExpireTime = DateTime.UtcNow.AddDays(_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);
|
|
|
|
|
|
}
|
2026-06-22 11:53:49 +06:00
|
|
|
|
|
2026-07-06 17:23:05 +06:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 11:53:49 +06:00
|
|
|
|
#endregion
|
2026-06-15 18:26:58 +06:00
|
|
|
|
}
|