This commit is contained in:
@@ -169,104 +169,110 @@ namespace AyaNova.Api.Controllers
|
|||||||
if (hashed == u.Password)
|
if (hashed == u.Password)
|
||||||
{
|
{
|
||||||
//Valid password, user is effectively authorized at this point
|
//Valid password, user is effectively authorized at this point
|
||||||
|
return await ReturnUserCredsOnSuccessfulAuthentication(u);
|
||||||
|
|
||||||
//check if server available to SuperUser account only (closed or migrate mode)
|
// //check if server available to SuperUser account only (closed or migrate mode)
|
||||||
//if it is it means we got here either because there is no license
|
// //if it is it means we got here either because there is no license
|
||||||
//and only *the* SuperUser account can login now or we're in migrate mode
|
// //and only *the* SuperUser account can login now or we're in migrate mode
|
||||||
if (serverState.IsClosed || serverState.IsMigrateMode)
|
// if (serverState.IsClosed || serverState.IsMigrateMode)
|
||||||
{
|
// {
|
||||||
//if not SuperUser account then boot closed
|
// //if not SuperUser account then boot closed
|
||||||
//SuperUser account is always ID 1
|
// //SuperUser account is always ID 1
|
||||||
if (u.Id != 1)
|
// if (u.Id != 1)
|
||||||
{
|
// {
|
||||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
// return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
//Restrict auth due to server state?
|
// //Restrict auth due to server state?
|
||||||
//If we're here the server state is not closed, but it might be ops only
|
// //If we're here the server state is not closed, but it might be ops only
|
||||||
|
|
||||||
//If the server is ops only then this user needs to be ops or else they are not allowed in
|
// //If the server is ops only then this user needs to be ops or else they are not allowed in
|
||||||
if (serverState.IsOpsOnly &&
|
// if (serverState.IsOpsOnly &&
|
||||||
!u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminFull) &&
|
// !u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminFull) &&
|
||||||
!u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminLimited))
|
// !u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminLimited))
|
||||||
{
|
// {
|
||||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
// return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
//TWO FACTOR ENABLED??
|
// //TWO FACTOR ENABLED??
|
||||||
//if 2fa enabled then need to validate it before sending token, so we're halfway there and need to send a 2fa prompt
|
// //if 2fa enabled then need to validate it before sending token, so we're halfway there and need to send a 2fa prompt
|
||||||
if (u.TwoFactorEnabled)
|
// if (u.TwoFactorEnabled)
|
||||||
{
|
// {
|
||||||
List<string> TranslationKeysToFetch = new List<string> { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel" };
|
// //Generate a temporary token to identify and verify this is the same user
|
||||||
var LT = await TranslationBiz.GetSubsetStaticAsync(TranslationKeysToFetch, u.UserOptions.TranslationId);
|
// u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", "");
|
||||||
|
// await ct.SaveChangesAsync();
|
||||||
|
|
||||||
return Ok(ApiOkResponse.Response(new
|
// List<string> TranslationKeysToFetch = new List<string> { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel" };
|
||||||
{
|
// var LT = await TranslationBiz.GetSubsetStaticAsync(TranslationKeysToFetch, u.UserOptions.TranslationId);
|
||||||
AuthTwoFactor = LT["AuthTwoFactor"],
|
|
||||||
AuthEnterPin = LT["AuthEnterPin"],
|
// return Ok(ApiOkResponse.Response(new
|
||||||
AuthVerifyCode = LT["AuthVerifyCode"],
|
// {
|
||||||
Cancel = LT["Cancel"],
|
// AuthTwoFactor = LT["AuthTwoFactor"],
|
||||||
tfa = true
|
// AuthEnterPin = LT["AuthEnterPin"],
|
||||||
}));
|
// AuthVerifyCode = LT["AuthVerifyCode"],
|
||||||
}
|
// Cancel = LT["Cancel"],
|
||||||
|
// tfa = true,
|
||||||
|
// tt = u.TempToken
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
//build the key (JWT set in startup.cs)
|
// //build the key (JWT set in startup.cs)
|
||||||
byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET);
|
// byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET);
|
||||||
|
|
||||||
//create a new datetime offset of now in utc time
|
// //create a new datetime offset of now in utc time
|
||||||
var iat = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);//timespan zero means zero time off utc / specifying this is a UTC datetime
|
// var iat = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);//timespan zero means zero time off utc / specifying this is a UTC datetime
|
||||||
var exp = new DateTimeOffset(DateTime.Now.AddDays(JWT_LIFETIME_DAYS).ToUniversalTime(), TimeSpan.Zero);
|
// var exp = new DateTimeOffset(DateTime.Now.AddDays(JWT_LIFETIME_DAYS).ToUniversalTime(), TimeSpan.Zero);
|
||||||
|
|
||||||
|
|
||||||
//=============== download token ===================
|
// //=============== download token ===================
|
||||||
//Generate a download token and store it with the user account
|
// //Generate a download token and store it with the user account
|
||||||
//string DownloadToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
|
// //string DownloadToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
|
||||||
string DownloadToken = Hasher.GenerateSalt();
|
// string DownloadToken = Hasher.GenerateSalt();
|
||||||
DownloadToken = DownloadToken.Replace("=", "");
|
// DownloadToken = DownloadToken.Replace("=", "");
|
||||||
DownloadToken = DownloadToken.Replace("+", "");
|
// DownloadToken = DownloadToken.Replace("+", "");
|
||||||
u.DlKey = DownloadToken;
|
// u.DlKey = DownloadToken;
|
||||||
u.DlKeyExpire = exp.DateTime;
|
// u.DlKeyExpire = exp.DateTime;
|
||||||
|
|
||||||
//=======================================================
|
// //=======================================================
|
||||||
|
|
||||||
var payload = new Dictionary<string, object>()
|
// var payload = new Dictionary<string, object>()
|
||||||
{
|
// {
|
||||||
// { "iat", iat.ToUnixTimeSeconds().ToString() },
|
// // { "iat", iat.ToUnixTimeSeconds().ToString() },
|
||||||
{ "exp", exp.ToUnixTimeSeconds().ToString() },//in payload exp must be in unix epoch time per standard
|
// { "exp", exp.ToUnixTimeSeconds().ToString() },//in payload exp must be in unix epoch time per standard
|
||||||
{ "iss", "ayanova.com" },
|
// { "iss", "ayanova.com" },
|
||||||
{ "id", u.Id.ToString() }
|
// { "id", u.Id.ToString() }
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
|
||||||
//NOTE: probably don't need Jose.JWT as am using Microsoft jwt stuff to validate routes so it should also be able to
|
// //NOTE: probably don't need Jose.JWT as am using Microsoft jwt stuff to validate routes so it should also be able to
|
||||||
//issue tokens as well, but it looked cmplex and this works so unless need to remove in future keeping it.
|
// //issue tokens as well, but it looked cmplex and this works so unless need to remove in future keeping it.
|
||||||
string token = Jose.JWT.Encode(payload, secretKey, Jose.JwsAlgorithm.HS256);
|
// string token = Jose.JWT.Encode(payload, secretKey, Jose.JwsAlgorithm.HS256);
|
||||||
|
|
||||||
//save auth token to ensure single sign on only
|
// //save auth token to ensure single sign on only
|
||||||
u.CurrentAuthToken = token;
|
// u.CurrentAuthToken = token;
|
||||||
|
|
||||||
u.LastLogin = DateTime.UtcNow;
|
// u.LastLogin = DateTime.UtcNow;
|
||||||
|
|
||||||
await ct.SaveChangesAsync();
|
// await ct.SaveChangesAsync();
|
||||||
|
|
||||||
//KEEP this, masked version of IP address
|
// //KEEP this, masked version of IP address
|
||||||
//Not sure if this is necessary or not but if it turns out to be then make it a boot option
|
// //Not sure if this is necessary or not but if it turns out to be then make it a boot option
|
||||||
// log.LogInformation($"User number \"{u.Id}\" logged in from \"{Util.StringUtil.MaskIPAddress(HttpContext.Connection.RemoteIpAddress.ToString())}\" ok");
|
// // log.LogInformation($"User number \"{u.Id}\" logged in from \"{Util.StringUtil.MaskIPAddress(HttpContext.Connection.RemoteIpAddress.ToString())}\" ok");
|
||||||
|
|
||||||
log.LogInformation($"User \"{u.Name}\" logged in from \"{HttpContext.Connection.RemoteIpAddress.ToString()}\" ok");
|
// log.LogInformation($"User \"{u.Name}\" logged in from \"{HttpContext.Connection.RemoteIpAddress.ToString()}\" ok");
|
||||||
|
|
||||||
|
|
||||||
return Ok(ApiOkResponse.Response(new
|
// return Ok(ApiOkResponse.Response(new
|
||||||
{
|
// {
|
||||||
token = token,
|
// token = token,
|
||||||
name = u.Name,
|
// name = u.Name,
|
||||||
usertype = u.UserType,
|
// usertype = u.UserType,
|
||||||
roles = ((int)u.Roles).ToString(),
|
// roles = ((int)u.Roles).ToString(),
|
||||||
dlt = DownloadToken,
|
// dlt = DownloadToken,
|
||||||
tfa = u.TwoFactorEnabled
|
// tfa = u.TwoFactorEnabled
|
||||||
}));
|
// }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +283,169 @@ namespace AyaNova.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify tfa code
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This route is used to authenticate to the AyaNova API via tfa code.
|
||||||
|
/// Once you have a token you need to include it in all requests that require authentication like this:
|
||||||
|
/// <code>Authorization: Bearer [TOKEN]</code>
|
||||||
|
/// Note the space between Bearer and the token. Also, do not include the square brackets
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="pin"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("tfa-authenticate")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> TfaAuthenticate([FromBody] TFAPinParam pin)
|
||||||
|
{
|
||||||
|
//a bit different as ops users can still login if the state is opsonly
|
||||||
|
//so the only real barrier here would be a completely closed api
|
||||||
|
|
||||||
|
|
||||||
|
if (serverState.IsClosed && AyaNova.Core.License.ActiveKey.KeyDoesNotNeedAttention)
|
||||||
|
{
|
||||||
|
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pin.Pin))
|
||||||
|
{
|
||||||
|
//Make a failed pw wait
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Match to temp token that would have been set by initial credentialed login for 2fa User
|
||||||
|
var user = await ct.User.Where(z => z.TempToken == pin.TempToken && z.Active == true && z.TwoFactorEnabled == true).FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
//Valid temp token, now check the pin code is right
|
||||||
|
if (string.IsNullOrWhiteSpace(user.TotpSecret))
|
||||||
|
{
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa not activated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
//ok, something to validate, let's validate it
|
||||||
|
var tfa = new TwoFactorAuth("AyaNova");
|
||||||
|
if (!tfa.VerifyCode(user.TotpSecret, pin.Pin.Replace(" ", "").Trim()))
|
||||||
|
{
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
|
}
|
||||||
|
|
||||||
|
//User is valid and authenticated
|
||||||
|
//clear temp token
|
||||||
|
user.TempToken = string.Empty;
|
||||||
|
await ct.SaveChangesAsync();
|
||||||
|
|
||||||
|
return await ReturnUserCredsOnSuccessfulAuthentication(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
//No users matched, it's a failed login
|
||||||
|
//Make a failed pw wait
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> ReturnUserCredsOnSuccessfulAuthentication(User u)
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
//check if server available to SuperUser account only (closed or migrate mode)
|
||||||
|
//if it is it means we got here either because there is no license
|
||||||
|
//and only *the* SuperUser account can login now or we're in migrate mode
|
||||||
|
if (serverState.IsClosed || serverState.IsMigrateMode)
|
||||||
|
{
|
||||||
|
//if not SuperUser account then boot closed
|
||||||
|
//SuperUser account is always ID 1
|
||||||
|
if (u.Id != 1)
|
||||||
|
{
|
||||||
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Restrict auth due to server state?
|
||||||
|
//If we're here the server state is not closed, but it might be ops only
|
||||||
|
|
||||||
|
//If the server is ops only then this user needs to be ops or else they are not allowed in
|
||||||
|
if (serverState.IsOpsOnly &&
|
||||||
|
!u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminFull) &&
|
||||||
|
!u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminLimited))
|
||||||
|
{
|
||||||
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//build the key (JWT set in startup.cs)
|
||||||
|
byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET);
|
||||||
|
|
||||||
|
//create a new datetime offset of now in utc time
|
||||||
|
var iat = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);//timespan zero means zero time off utc / specifying this is a UTC datetime
|
||||||
|
var exp = new DateTimeOffset(DateTime.Now.AddDays(JWT_LIFETIME_DAYS).ToUniversalTime(), TimeSpan.Zero);
|
||||||
|
|
||||||
|
|
||||||
|
//=============== download token ===================
|
||||||
|
//Generate a download token and store it with the user account
|
||||||
|
//string DownloadToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
|
||||||
|
string DownloadToken = Hasher.GenerateSalt();
|
||||||
|
DownloadToken = DownloadToken.Replace("=", "");
|
||||||
|
DownloadToken = DownloadToken.Replace("+", "");
|
||||||
|
u.DlKey = DownloadToken;
|
||||||
|
u.DlKeyExpire = exp.DateTime;
|
||||||
|
|
||||||
|
//=======================================================
|
||||||
|
|
||||||
|
var payload = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
// { "iat", iat.ToUnixTimeSeconds().ToString() },
|
||||||
|
{ "exp", exp.ToUnixTimeSeconds().ToString() },//in payload exp must be in unix epoch time per standard
|
||||||
|
{ "iss", "ayanova.com" },
|
||||||
|
{ "id", u.Id.ToString() }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//NOTE: probably don't need Jose.JWT as am using Microsoft jwt stuff to validate routes so it should also be able to
|
||||||
|
//issue tokens as well, but it looked cmplex and this works so unless need to remove in future keeping it.
|
||||||
|
string token = Jose.JWT.Encode(payload, secretKey, Jose.JwsAlgorithm.HS256);
|
||||||
|
|
||||||
|
//save auth token to ensure single sign on only
|
||||||
|
u.CurrentAuthToken = token;
|
||||||
|
|
||||||
|
u.LastLogin = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await ct.SaveChangesAsync();
|
||||||
|
|
||||||
|
//KEEP this, masked version of IP address
|
||||||
|
//Not sure if this is necessary or not but if it turns out to be then make it a boot option
|
||||||
|
// log.LogInformation($"User number \"{u.Id}\" logged in from \"{Util.StringUtil.MaskIPAddress(HttpContext.Connection.RemoteIpAddress.ToString())}\" ok");
|
||||||
|
|
||||||
|
log.LogInformation($"User \"{u.Name}\" logged in from \"{HttpContext.Connection.RemoteIpAddress.ToString()}\" ok");
|
||||||
|
|
||||||
|
|
||||||
|
return Ok(ApiOkResponse.Response(new
|
||||||
|
{
|
||||||
|
token = token,
|
||||||
|
name = u.Name,
|
||||||
|
usertype = u.UserType,
|
||||||
|
roles = ((int)u.Roles).ToString(),
|
||||||
|
dlt = DownloadToken,
|
||||||
|
tfa = u.TwoFactorEnabled
|
||||||
|
}));
|
||||||
|
|
||||||
|
//------------------------ /STANDARD BLOCK -------------------------
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Change Password
|
/// Change Password
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -294,12 +463,9 @@ namespace AyaNova.Api.Controllers
|
|||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
return BadRequest(new ApiErrorResponse(ModelState));
|
return BadRequest(new ApiErrorResponse(ModelState));
|
||||||
|
|
||||||
int nFailedAuthDelay = 3000;//should be just long enough to make brute force a hassle but short enough to not annoy people who just mistyped their creds to login
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(changecreds.OldPassword) || string.IsNullOrWhiteSpace(changecreds.LoginName))
|
if (string.IsNullOrWhiteSpace(changecreds.OldPassword) || string.IsNullOrWhiteSpace(changecreds.LoginName))
|
||||||
{
|
{
|
||||||
//Make a failed pw wait
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
await Task.Delay(nFailedAuthDelay);
|
|
||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +489,7 @@ namespace AyaNova.Api.Controllers
|
|||||||
if (!u.Active)
|
if (!u.Active)
|
||||||
{
|
{
|
||||||
//respond like bad creds so as not to leak information
|
//respond like bad creds so as not to leak information
|
||||||
await Task.Delay(nFailedAuthDelay);
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +497,7 @@ namespace AyaNova.Api.Controllers
|
|||||||
//otherwise it's feasible someone could change someone else's password through their own change password form with a mis-type or intentional hack
|
//otherwise it's feasible someone could change someone else's password through their own change password form with a mis-type or intentional hack
|
||||||
if (u.Id != UserIdFromContext.Id(HttpContext.Items))
|
if (u.Id != UserIdFromContext.Id(HttpContext.Items))
|
||||||
{
|
{
|
||||||
await Task.Delay(nFailedAuthDelay);
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,11 +513,9 @@ namespace AyaNova.Api.Controllers
|
|||||||
|
|
||||||
//No users matched, it's a failed login
|
//No users matched, it's a failed login
|
||||||
//Make a failed pw wait
|
//Make a failed pw wait
|
||||||
await Task.Delay(nFailedAuthDelay);
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
|
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
|
|
||||||
//return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -371,18 +535,17 @@ namespace AyaNova.Api.Controllers
|
|||||||
{
|
{
|
||||||
return BadRequest(new ApiErrorResponse(ModelState));
|
return BadRequest(new ApiErrorResponse(ModelState));
|
||||||
}
|
}
|
||||||
int nFailDelay = 3000;
|
|
||||||
if (string.IsNullOrWhiteSpace(resetcreds.PasswordResetCode))
|
if (string.IsNullOrWhiteSpace(resetcreds.PasswordResetCode))
|
||||||
{
|
{
|
||||||
//Make a fail wait
|
//Make a fail wait
|
||||||
await Task.Delay(nFailDelay);
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "PasswordResetCode", "Reset code is required"));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "PasswordResetCode", "Reset code is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(resetcreds.Password))
|
if (string.IsNullOrWhiteSpace(resetcreds.Password))
|
||||||
{
|
{
|
||||||
//Make a fail wait
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
await Task.Delay(nFailDelay);
|
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "Password", "Password is required"));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "Password", "Password is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,15 +553,13 @@ namespace AyaNova.Api.Controllers
|
|||||||
var user = await ct.User.AsNoTracking().Where(z => z.PasswordResetCode == resetcreds.PasswordResetCode).FirstOrDefaultAsync();
|
var user = await ct.User.AsNoTracking().Where(z => z.PasswordResetCode == resetcreds.PasswordResetCode).FirstOrDefaultAsync();
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
//Make a fail wait
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
await Task.Delay(nFailDelay);
|
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid"));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(user.PasswordResetCode) || user.PasswordResetCodeExpire == null)
|
if (string.IsNullOrWhiteSpace(user.PasswordResetCode) || user.PasswordResetCodeExpire == null)
|
||||||
{
|
{
|
||||||
//Make a fail wait
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
await Task.Delay(nFailDelay);
|
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid"));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,8 +567,7 @@ namespace AyaNova.Api.Controllers
|
|||||||
var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
|
var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
|
||||||
if (user.PasswordResetCodeExpire < utcNow.DateTime)
|
if (user.PasswordResetCodeExpire < utcNow.DateTime)
|
||||||
{//if reset code expired before now
|
{//if reset code expired before now
|
||||||
//Make a fail wait
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
await Task.Delay(nFailDelay);
|
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "PasswordResetCodeExpire", "Reset code has expired"));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "PasswordResetCodeExpire", "Reset code has expired"));
|
||||||
}
|
}
|
||||||
//Ok, were in, it's all good, accept the new password and update the user record
|
//Ok, were in, it's all good, accept the new password and update the user record
|
||||||
@@ -437,7 +597,10 @@ namespace AyaNova.Api.Controllers
|
|||||||
return BadRequest(new ApiErrorResponse(ModelState));
|
return BadRequest(new ApiErrorResponse(ModelState));
|
||||||
uint res = await biz.SendPasswordResetCode(id);
|
uint res = await biz.SendPasswordResetCode(id);
|
||||||
if (res == 0)
|
if (res == 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return BadRequest(new ApiErrorResponse(biz.Errors));
|
return BadRequest(new ApiErrorResponse(biz.Errors));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
return Ok(ApiOkResponse.Response(new
|
return Ok(ApiOkResponse.Response(new
|
||||||
{
|
{
|
||||||
@@ -502,7 +665,7 @@ namespace AyaNova.Api.Controllers
|
|||||||
/// <param name="apiVersion">From route path</param>
|
/// <param name="apiVersion">From route path</param>
|
||||||
/// <returns>OK on success</returns>
|
/// <returns>OK on success</returns>
|
||||||
[HttpPost("totp-validate")]
|
[HttpPost("totp-validate")]
|
||||||
public async Task<IActionResult> ValidateTOTP([FromBody] OTPPinParam pin, ApiVersion apiVersion)
|
public async Task<IActionResult> ValidateTOTP([FromBody] TFAPinParam pin, ApiVersion apiVersion)
|
||||||
{
|
{
|
||||||
if (!serverState.IsOpen)
|
if (!serverState.IsOpen)
|
||||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||||
@@ -515,13 +678,22 @@ namespace AyaNova.Api.Controllers
|
|||||||
|
|
||||||
var u = await ct.User.FirstOrDefaultAsync(z => z.Id == UserId);
|
var u = await ct.User.FirstOrDefaultAsync(z => z.Id == UserId);
|
||||||
if (u == null)//should never happen but ?
|
if (u == null)//should never happen but ?
|
||||||
|
{
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||||
|
}
|
||||||
|
|
||||||
if (u.TwoFactorEnabled)
|
if (u.TwoFactorEnabled)
|
||||||
|
{
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa already enabled"));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa already enabled"));
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(u.TotpSecret))
|
if (string.IsNullOrWhiteSpace(u.TotpSecret))
|
||||||
|
{
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa activation code not requested yet (missed a step?)"));
|
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa activation code not requested yet (missed a step?)"));
|
||||||
|
}
|
||||||
|
|
||||||
//ok, something to validate, let's validate it
|
//ok, something to validate, let's validate it
|
||||||
var tfa = new TwoFactorAuth("AyaNova");
|
var tfa = new TwoFactorAuth("AyaNova");
|
||||||
@@ -532,6 +704,10 @@ namespace AyaNova.Api.Controllers
|
|||||||
u.TwoFactorEnabled = true;
|
u.TwoFactorEnabled = true;
|
||||||
await ct.SaveChangesAsync();
|
await ct.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ApiOkResponse.Response(new
|
return Ok(ApiOkResponse.Response(new
|
||||||
{
|
{
|
||||||
@@ -601,10 +777,11 @@ namespace AyaNova.Api.Controllers
|
|||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OTPPinParam
|
public class TFAPinParam
|
||||||
{
|
{
|
||||||
[System.ComponentModel.DataAnnotations.Required]
|
[System.ComponentModel.DataAnnotations.Required]
|
||||||
public string Pin { get; set; }
|
public string Pin { get; set; }
|
||||||
|
public string TempToken { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ namespace AyaNova.Models
|
|||||||
public DateTime? PasswordResetCodeExpire { get; set; }//---
|
public DateTime? PasswordResetCodeExpire { get; set; }//---
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string TotpSecret { get; set; }//---
|
public string TotpSecret { get; set; }//---
|
||||||
|
[JsonIgnore]
|
||||||
|
public string TempToken { get; set; }//--- Used for TFA verification step 2
|
||||||
//==========================
|
//==========================
|
||||||
|
|
||||||
//relations
|
//relations
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
|
|||||||
//Add user table
|
//Add user table
|
||||||
await ExecQueryAsync("CREATE TABLE auser (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, active BOOL NOT NULL, name TEXT NOT NULL UNIQUE, "
|
await ExecQueryAsync("CREATE TABLE auser (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, active BOOL NOT NULL, name TEXT NOT NULL UNIQUE, "
|
||||||
+ "lastlogin TIMESTAMP, login TEXT NOT NULL UNIQUE, password TEXT NOT NULL, salt TEXT NOT NULL, roles INTEGER NOT NULL, currentauthtoken TEXT, "
|
+ "lastlogin TIMESTAMP, login TEXT NOT NULL UNIQUE, password TEXT NOT NULL, salt TEXT NOT NULL, roles INTEGER NOT NULL, currentauthtoken TEXT, "
|
||||||
+ "dlkey TEXT, dlkeyexpire TIMESTAMP, totpsecret TEXT, twofactorenabled BOOL, passwordresetcode TEXT, passwordresetcodeexpire TIMESTAMP, usertype INTEGER NOT NULL, "
|
+ "dlkey TEXT, dlkeyexpire TIMESTAMP, totpsecret TEXT, temptoken TEXT, twofactorenabled BOOL, passwordresetcode TEXT, passwordresetcodeexpire TIMESTAMP, usertype INTEGER NOT NULL, "
|
||||||
+ "employeenumber TEXT, notes TEXT, customerid BIGINT, "
|
+ "employeenumber TEXT, notes TEXT, customerid BIGINT, "
|
||||||
+ "headofficeid BIGINT, vendorid BIGINT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY)");
|
+ "headofficeid BIGINT, vendorid BIGINT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY)");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user