using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using TwoFactorAuthNet; using QRCoder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using AyaNova.Models; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using System.Linq; using System; using System.Threading.Tasks; using AyaNova.Biz; //required to inject configuration in constructor using Microsoft.Extensions.Configuration; namespace AyaNova.Api.Controllers { /// /// Authentication controller /// [ApiController] [ApiVersion("8.0")] [Route("api/v{version:apiVersion}/auth")] [Produces("application/json")] [Authorize] public class AuthController : ControllerBase { private readonly AyContext ct; private readonly ILogger log; private readonly IConfiguration _configuration; private readonly ApiServerState serverState; private const int JWT_LIFETIME_DAYS = 7; /// /// ctor /// /// /// /// /// public AuthController(AyContext context, ILogger logger, IConfiguration configuration, ApiServerState apiServerState) { ct = context; log = logger; _configuration = configuration; serverState = apiServerState; } //AUTHENTICATE CREDS //RETURN JWT /// /// Create credentials to receive a JSON web token /// /// /// This route is used to authenticate to the AyaNova API. /// Once you have a token you need to include it in all requests that require authentication like this: /// Authorization: Bearer [TOKEN] /// Note the space between Bearer and the token. Also, do not include the square brackets /// /// /// [HttpPost] [AllowAnonymous] public async Task PostCreds([FromBody] AuthController.CredentialsParam creds) //if was a json body then //public JsonResult PostCreds([FromBody] string login, [FromBody] string password) { //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 (DEBUG) #region TESTING //TEST JWT's with various flaws for testing purposes: if (creds.Login == "INTEGRATION_TEST") { //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(30).ToUniversalTime(), TimeSpan.Zero); string Issuer = "ayanova.com"; var Algorithm = Jose.JwsAlgorithm.HS256; //Pre JWT creation test payloads switch (creds.Password) { case "EXPIRED": exp = new DateTimeOffset(DateTime.Now.AddDays(-30).ToUniversalTime(), TimeSpan.Zero); break; case "WRONG_ISSUER": Issuer = "Bogus"; break; case "NO_ALGORITHM": Algorithm = Jose.JwsAlgorithm.none; break; case "WRONG_SECRET": secretKey = System.Text.Encoding.ASCII.GetBytes("xxxxxxThisIsObviouslyWrongxxxxxx"); break; } var payload = new Dictionary() { //{ "iat", iat.ToUnixTimeSeconds().ToString() }, { "exp", exp.ToUnixTimeSeconds().ToString() },//in payload exp must be in unix epoch time per standard { "iss", Issuer }, { "id", "1" } }; string TestToken = Jose.JWT.Encode(payload, secretKey, Algorithm); //Post JWT creation test payloads switch (creds.Password) { case "TRUNCATED_SIGNATURE": TestToken = TestToken.Substring(0, TestToken.Length - 3); break; case "TRANSPOSE_SIGNATURE": //Transpose two characters in the signature int len = TestToken.Length; var Transposed = TestToken.Substring(0, len - 5) + TestToken[len - 4] + TestToken[len - 5] + TestToken.Substring(len - 3, 3); TestToken = Transposed; break; } return Ok(ApiOkResponse.Response(new { token = TestToken, name = "SuperUser Account - TESTING", roles = "0" })); } #endregion testing #endif if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password)) { //Make a failed pw wait await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } //Multiple users are allowed the same password and login //Salt will differentiate them so get all users that match login, then try to match pw var users = await ct.User.Where(z => z.Login == creds.Login && z.Active == true).ToListAsync(); foreach (User u in users) { string hashed = Hasher.hash(u.Salt, creds.Password); if (hashed == u.Password) { //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) // //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)); // } // //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 (u.TwoFactorEnabled) // { // //Generate a temporary token to identify and verify this is the same user // u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", ""); // await ct.SaveChangesAsync(); // List TranslationKeysToFetch = new List { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel" }; // var LT = await TranslationBiz.GetSubsetStaticAsync(TranslationKeysToFetch, u.UserOptions.TranslationId); // return Ok(ApiOkResponse.Response(new // { // AuthTwoFactor = LT["AuthTwoFactor"], // AuthEnterPin = LT["AuthEnterPin"], // AuthVerifyCode = LT["AuthVerifyCode"], // Cancel = LT["Cancel"], // tfa = true, // tt = u.TempToken // })); // } // //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() // { // // { "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 // })); } } //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)); } /// /// Verify tfa code /// /// /// 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: /// Authorization: Bearer [TOKEN] /// Note the space between Bearer and the token. Also, do not include the square brackets /// /// /// [HttpPost("tfa-authenticate")] [AllowAnonymous] public async Task 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 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() { // { "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 ------------------------- } /// /// Change Password /// /// /// [HttpPost("change-password")] public async Task ChangePassword([FromBody] AuthController.ChangePasswordParam changecreds) { //Note: need to be authenticated to use this, only called from own user's UI //it still asks for old creds in case someone attempts to do this on another user's logged in session //Also it checks here that this is in fact the same user account calling this method as the user attempting to be modified if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); if (string.IsNullOrWhiteSpace(changecreds.OldPassword) || string.IsNullOrWhiteSpace(changecreds.LoginName)) { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } if (string.IsNullOrWhiteSpace(changecreds.NewPassword)) return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "NewPassword")); if (changecreds.NewPassword != changecreds.ConfirmPassword) return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "ConfirmPassword", "NewPassword does not match ConfirmPassword")); //Multiple users are allowed the same password and login //Salt will differentiate them so get all users that match login, then try to match pw var users = await ct.User.AsNoTracking().Where(z => z.Login == changecreds.LoginName).ToListAsync(); foreach (User u in users) { string hashed = Hasher.hash(u.Salt, changecreds.OldPassword); if (hashed == u.Password) { //If the user is inactive they may not login if (!u.Active) { //respond like bad creds so as not to leak information await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } //double check it's the currently logged in User's own User object only //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)) { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } //fetch and update user //Instantiate the business object handler UserBiz biz = UserBiz.GetBiz(ct, HttpContext); await biz.ChangePasswordAsync(u.Id, changecreds.NewPassword); return NoContent(); } } //No users matched, it's a failed login //Make a failed pw wait await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return BadRequest(new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } /// /// Change Password via reset token /// /// /// [HttpPost("reset-password")] [AllowAnonymous] public async Task ResetPassword([FromBody] AuthController.ResetPasswordParam resetcreds) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) { return BadRequest(new ApiErrorResponse(ModelState)); } if (string.IsNullOrWhiteSpace(resetcreds.PasswordResetCode)) { //Make a fail wait await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "PasswordResetCode", "Reset code is required")); } if (string.IsNullOrWhiteSpace(resetcreds.Password)) { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "Password", "Password is required")); } //look for user with this reset code var user = await ct.User.AsNoTracking().Where(z => z.PasswordResetCode == resetcreds.PasswordResetCode).FirstOrDefaultAsync(); if (user == null) { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid")); } if (string.IsNullOrWhiteSpace(user.PasswordResetCode) || user.PasswordResetCodeExpire == null) { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "PasswordResetCode", "Reset code not valid")); } //vet the expiry var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero); if (user.PasswordResetCodeExpire < utcNow.DateTime) {//if reset code expired before now await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); 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 UserBiz biz = UserBiz.GetBiz(ct, HttpContext); await biz.ChangePasswordAsync(user.Id, resetcreds.Password); return NoContent(); } /// /// Generate time limited password reset code for User /// and email link to them so they can set their password /// /// /// User id /// From route path /// New concurrency code [HttpPost("request-reset-password/{id}")] public async Task SendPasswordResetCode([FromRoute] long id, ApiVersion apiVersion) { //Note: this is not allowed for an anonymous users because it's only intended for now to work for staff user's who will send the request on behalf of the User if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); UserBiz biz = UserBiz.GetBiz(ct, HttpContext); if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); uint res = await biz.SendPasswordResetCode(id); if (res == 0) { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return BadRequest(new ApiErrorResponse(biz.Errors)); } else return Ok(ApiOkResponse.Response(new { concurrency = res })); } /// /// Generate TOTP secret and return for use in auth app /// /// /// From route path /// Authentication app activation code [HttpGet("totp")] public async Task GenerateAndSendTOTP(ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); UserBiz biz = UserBiz.GetBiz(ct, HttpContext); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); //get user and save the secret var UserId = UserIdFromContext.Id(HttpContext.Items); var u = await ct.User.FirstOrDefaultAsync(z => z.Id == UserId); if (u == null)//should never happen but ? return StatusCode(403, new ApiNotAuthorizedResponse()); //this is to stop someone from messing up someone's login accidentally or maliciously by simply hitting the route logged in as them if (u.TwoFactorEnabled) return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa already enabled")); var tfa = new TwoFactorAuth("AyaNova"); u.TotpSecret = tfa.CreateSecret(160); await ct.SaveChangesAsync(); //https://github.com/google/google-authenticator/wiki/Key-Uri-Format //otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 //this format tested and works with Google, Microsoft Authy, Duo authenticators string payload = $"otpauth://totp/AyaNova:{u.Name}?secret={u.TotpSecret}&issuer=AyaNova&algorithm=SHA1&digits=6&period=30"; QRCodeGenerator qrGenerator = new QRCodeGenerator(); QRCodeData qrCodeData = qrGenerator.CreateQrCode(payload, QRCodeGenerator.ECCLevel.Q); Base64QRCode qrCode = new Base64QRCode(qrCodeData); string qrCodeImageAsBase64 = qrCode.GetGraphic(3); return Ok(ApiOkResponse.Response(new { s = u.TotpSecret, qr = qrCodeImageAsBase64 })); } /// /// Confirm 2fa ready to use /// /// /// Auth app 6 digit passcode /// From route path /// OK on success [HttpPost("totp-validate")] public async Task ValidateTOTP([FromBody] TFAPinParam pin, ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); //get user var UserId = UserIdFromContext.Id(HttpContext.Items); var u = await ct.User.FirstOrDefaultAsync(z => z.Id == UserId); if (u == null)//should never happen but ? { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(403, new ApiNotAuthorizedResponse()); } if (u.TwoFactorEnabled) { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "generalerror", "2fa already enabled")); } 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?)")); } //ok, something to validate, let's validate it var tfa = new TwoFactorAuth("AyaNova"); var ret = tfa.VerifyCode(u.TotpSecret, pin.Pin.Replace(" ", "").Trim()); if (ret == true) { //enable 2fa on user account u.TwoFactorEnabled = true; await ct.SaveChangesAsync(); } else { await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); } return Ok(ApiOkResponse.Response(new { ok = ret })); } /// /// Disable (turn off) 2fa for current user account /// /// /// From route path /// OK on success [HttpPost("totp-disable")] public async Task DisableTOTP(ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); //get user var UserId = UserIdFromContext.Id(HttpContext.Items); var u = await ct.User.FirstOrDefaultAsync(z => z.Id == UserId); if (u == null)//should never happen but ? return StatusCode(403, new ApiNotAuthorizedResponse()); u.TotpSecret = null; u.TwoFactorEnabled = false; await ct.SaveChangesAsync(); return NoContent(); } //------------------------------------------------------ public class CredentialsParam { [System.ComponentModel.DataAnnotations.Required] public string Login { get; set; } [System.ComponentModel.DataAnnotations.Required] public string Password { get; set; } } public class ChangePasswordParam { [System.ComponentModel.DataAnnotations.Required] public string LoginName { get; set; } [System.ComponentModel.DataAnnotations.Required] public string OldPassword { get; set; } [System.ComponentModel.DataAnnotations.Required] public string NewPassword { get; set; } [System.ComponentModel.DataAnnotations.Required] public string ConfirmPassword { get; set; } } public class ResetPasswordParam { [System.ComponentModel.DataAnnotations.Required] public string PasswordResetCode { get; set; } [System.ComponentModel.DataAnnotations.Required] public string Password { get; set; } } public class TFAPinParam { [System.ComponentModel.DataAnnotations.Required] public string Pin { get; set; } public string TempToken { get; set; } } }//eoc }//eons