add refresh token
This commit is contained in:
parent
ade62e7b6a
commit
c5a333dcef
|
|
@ -14,9 +14,9 @@ public class IntegrationLoginRequest
|
|||
|
||||
[Required, NotNull, StringLength(maximumLength: 150, MinimumLength = 5, ErrorMessage = "Password must be between 1 and 30 characters.")]
|
||||
public string Password { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class IntegrationRefreshTokenRequest
|
||||
{
|
||||
public string RefreshToken { get; set; }
|
||||
|
|
@ -35,7 +35,7 @@ public class RevokedRefreshTokenRequest
|
|||
|
||||
public class GenerateRefreshTokenRequest
|
||||
{
|
||||
public User User { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public string IpAddress { get; set; }
|
||||
public string RawRefreshToken { get; set; }
|
||||
}
|
||||
|
|
@ -14,10 +14,16 @@ public class IntegrationLoginResponse : ResponseBase
|
|||
public DateTime AccessTokenExpiry { get; set; }
|
||||
}
|
||||
|
||||
public class IntegrationRrefreshTokenResponse : ResponseBase
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
|
||||
public class RefreshTokenResponse : ResponseBase
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public string TokenHash { get; set; }
|
||||
public string IpAddress { get; set; }
|
||||
public DateTime ExpiredAt { get; set; }
|
||||
|
|
@ -31,3 +37,12 @@ public class GenerateRefreshTokenResponse : ResponseBase
|
|||
public string RefreshToken { get; set; }
|
||||
public DateTime ExpireTime { get; set; }
|
||||
}
|
||||
|
||||
public class UserByRefreshTokenResponse : ResponseBase
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public string LoginId { get; set; }
|
||||
public string EmailAddress { get; set; }
|
||||
public string AuthKey { get; set; }
|
||||
public bool IsActive { set; get; }
|
||||
}
|
||||
|
|
@ -10,5 +10,7 @@ public interface IRefreshTokenService
|
|||
Task<bool> AddAsync(InsertRefreshTokenRequest refreshToken);
|
||||
Task<bool> RevokeAsync(RevokedRefreshTokenRequest token);
|
||||
Task<bool> RevokeAllForUserAsync(int userId);
|
||||
Task<GenerateRefreshTokenResponse> GenerateRefreshToken(GenerateRefreshTokenRequest request);
|
||||
Task<GenerateRefreshTokenResponse> GenerateRefreshTokenByUserAsync(GenerateRefreshTokenRequest request);
|
||||
Task<string> GenerateRefreshTokenAsync(string refreshToken, string ipAddress);
|
||||
Task<UserByRefreshTokenResponse> GetUserByRefreshTokenAsync(string refreshToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,33 @@ public class RefreshTokenService : IRefreshTokenService
|
|||
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode);
|
||||
try
|
||||
{
|
||||
await GetByTokenHashAsync(tc, tokenHash);
|
||||
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);
|
||||
tc.End();
|
||||
}
|
||||
catch (Exception ie)
|
||||
|
|
@ -108,7 +134,7 @@ public class RefreshTokenService : IRefreshTokenService
|
|||
{
|
||||
if (dr.Read())
|
||||
{
|
||||
response.UserId = dr.GetString(0);
|
||||
response.UserId = dr.GetInt32(0);
|
||||
response.TokenHash = dr.GetString(1);
|
||||
response.IpAddress = dr.GetString(2);
|
||||
response.ExpiredAt = dr.GetDateTime(3);
|
||||
|
|
@ -125,6 +151,36 @@ public class RefreshTokenService : IRefreshTokenService
|
|||
return response;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<bool> RevokeAllForUserAsync(int userId)
|
||||
{
|
||||
bool returnValue = false;
|
||||
|
|
@ -206,7 +262,31 @@ public class RefreshTokenService : IRefreshTokenService
|
|||
|
||||
return returnValue;
|
||||
}
|
||||
public async Task<GenerateRefreshTokenResponse> GenerateRefreshToken(GenerateRefreshTokenRequest request)
|
||||
|
||||
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)
|
||||
{
|
||||
GenerateRefreshTokenResponse refreshTokenResponse = new();
|
||||
try
|
||||
|
|
@ -214,17 +294,10 @@ public class RefreshTokenService : IRefreshTokenService
|
|||
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
|
||||
try
|
||||
{
|
||||
var tokenHash = HashToken(request.RawRefreshToken);
|
||||
|
||||
var storedToken = await GetByTokenHashAsync(tc, tokenHash);
|
||||
|
||||
if (storedToken.UserId is not null && !storedToken.IsActive)
|
||||
throw new UnauthorizedAccessException("Refresh token has expired or been revoked.");
|
||||
|
||||
// Rotate: revoke old token, issue new one
|
||||
RevokeAsync(tc,new RevokedRefreshTokenRequest() { RefreshToken = storedToken.TokenHash });
|
||||
RevokeRefreshTokenByUserIdAsync(tc, request.UserId);
|
||||
|
||||
refreshTokenResponse= await IssueTokensAsync(tc,request.User, request.IpAddress);
|
||||
refreshTokenResponse= await IssueTokensAsync(tc,request.UserId, request.IpAddress);
|
||||
|
||||
tc.End();
|
||||
}
|
||||
|
|
@ -243,16 +316,68 @@ public class RefreshTokenService : IRefreshTokenService
|
|||
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;
|
||||
}
|
||||
|
||||
// ----- private helpers -----
|
||||
|
||||
private async Task<GenerateRefreshTokenResponse> IssueTokensAsync(TransactionContext tc, User user, string ipAddress)
|
||||
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)
|
||||
{
|
||||
string rawRefreshToken = GenerateRowToken();
|
||||
string tokenHash = HashToken(rawRefreshToken);
|
||||
var refreshToken = new InsertRefreshTokenRequest
|
||||
{
|
||||
UserId = user.UserId,
|
||||
TokenHash = HashToken(rawRefreshToken),
|
||||
UserId = userId,
|
||||
TokenHash = tokenHash,
|
||||
IpAddress = ipAddress,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(_settings.RefreshTokenDuration)
|
||||
|
|
@ -262,7 +387,7 @@ public class RefreshTokenService : IRefreshTokenService
|
|||
|
||||
return new GenerateRefreshTokenResponse
|
||||
{
|
||||
RefreshToken = rawRefreshToken,
|
||||
RefreshToken = tokenHash,
|
||||
ExpireTime = DateTime.UtcNow.AddMinutes(_settings.RefreshTokenDuration)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user