This commit is contained in:
2021-03-12 17:57:50 +00:00
parent 00497abf6a
commit 1dc7d73bb9
3 changed files with 279 additions and 100 deletions

View File

@@ -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; }
} }

View File

@@ -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

View File

@@ -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)");