2 factor authentication and user account lockout completed
This commit is contained in:
@ -97,23 +97,30 @@ namespace Oqtane.Controllers
|
||||
|
||||
private User Filter(User user)
|
||||
{
|
||||
if (user != null && !User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
|
||||
if (user != null)
|
||||
{
|
||||
user.DisplayName = "";
|
||||
user.Email = "";
|
||||
user.PhotoFileId = null;
|
||||
user.LastLoginOn = DateTime.MinValue;
|
||||
user.LastIPAddress = "";
|
||||
user.Roles = "";
|
||||
user.CreatedBy = "";
|
||||
user.CreatedOn = DateTime.MinValue;
|
||||
user.ModifiedBy = "";
|
||||
user.ModifiedOn = DateTime.MinValue;
|
||||
user.DeletedBy = "";
|
||||
user.DeletedOn = DateTime.MinValue;
|
||||
user.IsDeleted = false;
|
||||
user.Password = "";
|
||||
user.IsAuthenticated = false;
|
||||
user.TwoFactorCode = "";
|
||||
user.TwoFactorExpiry = null;
|
||||
|
||||
if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
|
||||
{
|
||||
user.DisplayName = "";
|
||||
user.Email = "";
|
||||
user.PhotoFileId = null;
|
||||
user.LastLoginOn = DateTime.MinValue;
|
||||
user.LastIPAddress = "";
|
||||
user.Roles = "";
|
||||
user.CreatedBy = "";
|
||||
user.CreatedOn = DateTime.MinValue;
|
||||
user.ModifiedBy = "";
|
||||
user.ModifiedOn = DateTime.MinValue;
|
||||
user.DeletedBy = "";
|
||||
user.DeletedOn = DateTime.MinValue;
|
||||
user.IsDeleted = false;
|
||||
user.TwoFactorRequired = false;
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@ -247,15 +254,14 @@ namespace Oqtane.Controllers
|
||||
{
|
||||
if (ModelState.IsValid && user.SiteId == _alias.SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username))
|
||||
{
|
||||
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
||||
if (identityuser != null)
|
||||
if (user.Password != "")
|
||||
{
|
||||
identityuser.TwoFactorEnabled = user.TwoFactorEnabled;
|
||||
if (user.Password != "")
|
||||
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
||||
if (identityuser != null)
|
||||
{
|
||||
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
|
||||
await _identityUserManager.UpdateAsync(identityuser);
|
||||
}
|
||||
await _identityUserManager.UpdateAsync(identityuser);
|
||||
}
|
||||
user = _users.UpdateUser(user);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, user.UserId);
|
||||
@ -333,7 +339,7 @@ namespace Oqtane.Controllers
|
||||
[HttpPost("login")]
|
||||
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent)
|
||||
{
|
||||
User loginUser = new User { Username = user.Username, IsAuthenticated = false };
|
||||
User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
@ -343,24 +349,44 @@ namespace Oqtane.Controllers
|
||||
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
loginUser = _users.GetUser(identityuser.UserName);
|
||||
if (loginUser != null)
|
||||
user = _users.GetUser(user.Username);
|
||||
if (user.TwoFactorRequired)
|
||||
{
|
||||
if (identityuser.EmailConfirmed)
|
||||
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
|
||||
user.TwoFactorCode = token;
|
||||
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
|
||||
_users.UpdateUser(user);
|
||||
|
||||
string body = "Dear " + user.DisplayName + ",\n\nYou requested a secure verification code to log in to your account. Please enter the secure verification code on the site:\n\n" + token +
|
||||
"\n\nPlease note that the code is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate a new login on the site." +
|
||||
"\n\nThank You!";
|
||||
var notification = new Notification(loginUser.SiteId, user, "User Verification Code", body);
|
||||
_notifications.AddNotification(notification);
|
||||
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Verification Notification Sent For {Username}", user.Username);
|
||||
loginUser.TwoFactorRequired = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
loginUser = _users.GetUser(identityuser.UserName);
|
||||
if (loginUser != null)
|
||||
{
|
||||
loginUser.IsAuthenticated = true;
|
||||
loginUser.LastLoginOn = DateTime.UtcNow;
|
||||
loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString();
|
||||
_users.UpdateUser(loginUser);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
|
||||
if (setCookie)
|
||||
if (identityuser.EmailConfirmed)
|
||||
{
|
||||
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
|
||||
loginUser.IsAuthenticated = true;
|
||||
loginUser.LastLoginOn = DateTime.UtcNow;
|
||||
loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString();
|
||||
_users.UpdateUser(loginUser);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
|
||||
if (setCookie)
|
||||
{
|
||||
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -371,16 +397,16 @@ namespace Oqtane.Controllers
|
||||
user = _users.GetUser(user.Username);
|
||||
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
|
||||
string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
|
||||
string body = "Dear " + user.DisplayName + ",\n\nYou attempted 3 times unsuccessfully to login to your account and it is now locked out. Please wait 10 minutes and then try again... or use the link below to reset your password:\n\n" + url +
|
||||
string body = "Dear " + user.DisplayName + ",\n\nYou attempted 3 times unsuccessfully to log in to your account and it is now locked out. Please wait 10 minutes and then try again... or use the link below to reset your password:\n\n" + url +
|
||||
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
|
||||
"\n\nThank You!";
|
||||
var notification = new Notification(user.SiteId, user, "User Password Lockout", body);
|
||||
var notification = new Notification(loginUser.SiteId, user, "User Lockout", body);
|
||||
_notifications.AddNotification(notification);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Lockout Notification Sent For {Username}", user.Username);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Lockout Notification Sent For {Username}", user.Username);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "User Login Failed {Username}", user.Username);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed {Username}", user.Username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -485,6 +511,27 @@ namespace Oqtane.Controllers
|
||||
return user;
|
||||
}
|
||||
|
||||
// POST api/<controller>/twofactor
|
||||
[HttpPost("twofactor")]
|
||||
public User TwoFactor([FromBody] User user, string token)
|
||||
{
|
||||
User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
|
||||
|
||||
if (ModelState.IsValid && !string.IsNullOrEmpty(token))
|
||||
{
|
||||
user = _users.GetUser(user.Username);
|
||||
if (user != null)
|
||||
{
|
||||
if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
|
||||
{
|
||||
loginUser.IsAuthenticated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
// GET api/<controller>/authenticate
|
||||
[HttpGet("authenticate")]
|
||||
public User Authenticate()
|
||||
|
@ -141,9 +141,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
options.Password.RequireLowercase = false;
|
||||
|
||||
// Lockout settings
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
|
||||
options.Lockout.MaxFailedAccessAttempts = 10;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
|
||||
options.Lockout.MaxFailedAccessAttempts = 3;
|
||||
options.Lockout.AllowedForNewUsers = false;
|
||||
|
||||
// User settings
|
||||
options.User.RequireUniqueEmail = false;
|
||||
|
@ -52,64 +52,119 @@ namespace Oqtane.Migrations.EntityBuilders
|
||||
_migrationBuilder.AddColumn<bool>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
|
||||
}
|
||||
|
||||
public void AddBooleanColumn(string name, bool nullable, bool defaultValue)
|
||||
{
|
||||
_migrationBuilder.AddColumn<bool>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddBooleanColumn(ColumnsBuilder table, string name, bool nullable = false)
|
||||
{
|
||||
return table.Column<bool>(name: RewriteName(name), nullable: nullable);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddBooleanColumn(ColumnsBuilder table, string name, bool nullable, bool defaultValue)
|
||||
{
|
||||
return table.Column<bool>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public void AddDateTimeColumn(string name, bool nullable = false)
|
||||
{
|
||||
_migrationBuilder.AddColumn<DateTime>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
|
||||
}
|
||||
|
||||
public void AddDateTimeColumn(string name, bool nullable, DateTime defaultValue)
|
||||
{
|
||||
_migrationBuilder.AddColumn<DateTime>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable = false)
|
||||
{
|
||||
return table.Column<DateTime>(name: RewriteName(name), nullable: nullable);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable, DateTime defaultValue)
|
||||
{
|
||||
return table.Column<DateTime>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public void AddDateTimeOffsetColumn(string name, bool nullable = false)
|
||||
{
|
||||
_migrationBuilder.AddColumn<DateTimeOffset>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
|
||||
}
|
||||
|
||||
public void AddDateTimeOffsetColumn(string name, bool nullable, DateTimeOffset defaultValue)
|
||||
{
|
||||
_migrationBuilder.AddColumn<DateTimeOffset>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable = false)
|
||||
{
|
||||
return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable, DateTimeOffset defaultValue)
|
||||
{
|
||||
return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public void AddIntegerColumn(string name, bool nullable = false)
|
||||
{
|
||||
_migrationBuilder.AddColumn<int>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
|
||||
}
|
||||
|
||||
public void AddIntegerColumn(string name, bool nullable, int defaultValue)
|
||||
{
|
||||
_migrationBuilder.AddColumn<int>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddIntegerColumn(ColumnsBuilder table, string name, bool nullable = false)
|
||||
{
|
||||
return table.Column<int>(name: RewriteName(name), nullable: nullable);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddIntegerColumn(ColumnsBuilder table, string name, bool nullable, int defaultValue)
|
||||
{
|
||||
return table.Column<int>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true)
|
||||
{
|
||||
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode);
|
||||
}
|
||||
|
||||
public void AddMaxStringColumn(string name, bool nullable, bool unicode, string defaultValue)
|
||||
{
|
||||
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable = false, bool unicode = true)
|
||||
{
|
||||
return table.Column<string>(name: RewriteName(name), nullable: nullable, unicode: unicode);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable, bool unicode, string defaultValue)
|
||||
{
|
||||
return table.Column<string>(name: RewriteName(name), nullable: nullable, unicode: unicode, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true)
|
||||
{
|
||||
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode);
|
||||
}
|
||||
|
||||
public void AddStringColumn(string name, int length, bool nullable, bool unicode, string defaultValue)
|
||||
{
|
||||
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable = false, bool unicode = true)
|
||||
{
|
||||
return table.Column<string>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode);
|
||||
}
|
||||
|
||||
public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true)
|
||||
protected OperationBuilder<AddColumnOperation> AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable, bool unicode, string defaultValue)
|
||||
{
|
||||
ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode);
|
||||
return table.Column<string>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false)
|
||||
@ -117,11 +172,26 @@ namespace Oqtane.Migrations.EntityBuilders
|
||||
_migrationBuilder.AddColumn<decimal>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale);
|
||||
}
|
||||
|
||||
public void AddDecimalColumn(string name, int precision, int scale, bool nullable, decimal defaultValue)
|
||||
{
|
||||
_migrationBuilder.AddColumn<decimal>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable = false)
|
||||
{
|
||||
return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale);
|
||||
}
|
||||
|
||||
protected OperationBuilder<AddColumnOperation> AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable, decimal defaultValue)
|
||||
{
|
||||
return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true)
|
||||
{
|
||||
ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode);
|
||||
}
|
||||
|
||||
public void DropColumn(string name)
|
||||
{
|
||||
ActiveDatabase.DropColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName));
|
||||
|
33
Oqtane.Server/Migrations/Tenant/03010002_AddUserTwoFactor.cs
Normal file
33
Oqtane.Server/Migrations/Tenant/03010002_AddUserTwoFactor.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
using Oqtane.Migrations.EntityBuilders;
|
||||
using Oqtane.Repository;
|
||||
|
||||
namespace Oqtane.Migrations.Tenant
|
||||
{
|
||||
[DbContext(typeof(TenantDBContext))]
|
||||
[Migration("Tenant.03.01.00.02")]
|
||||
public class AddUserTwoFactor : MultiDatabaseMigration
|
||||
{
|
||||
public AddUserTwoFactor(IDatabase database) : base(database)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
userEntityBuilder.AddBooleanColumn("TwoFactorRequired", false, false);
|
||||
userEntityBuilder.AddStringColumn("TwoFactorCode", 6, true);
|
||||
userEntityBuilder.AddDateTimeColumn("TwoFactorExpiry", true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
userEntityBuilder.DropColumn("TwoFactorRequired");
|
||||
userEntityBuilder.DropColumn("TwoFactorCode");
|
||||
userEntityBuilder.DropColumn("TwoFactorExpiry");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
/* Login Module Custom Styles */
|
||||
|
||||
.Oqtane-Modules-Admin-Login .username {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.Oqtane-Modules-Admin-Login .password {
|
||||
.Oqtane-Modules-Admin-Login .input {
|
||||
width: 200px;
|
||||
}
|
||||
|
Reference in New Issue
Block a user