Merge pull request #1149 from LogicalPhallacy/ImprovedPasswordReset
Adds per user password reset
This commit is contained in:
commit
72dd609109
|
@ -1124,7 +1124,7 @@ namespace Emby.Server.Implementations
|
|||
MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
|
||||
|
||||
NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
||||
UserManager.AddParts(GetExports<IAuthenticationProvider>());
|
||||
UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
|
||||
|
||||
IsoManager.AddParts(GetExports<IIsoMounter>());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public class DefaultPasswordResetProvider : IPasswordResetProvider
|
||||
{
|
||||
public string Name => "Default Password Reset Provider";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
private readonly string _passwordResetFileBase;
|
||||
private readonly string _passwordResetFileBaseDir;
|
||||
private readonly string _passwordResetFileBaseName = "passwordreset";
|
||||
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ICryptoProvider _crypto;
|
||||
|
||||
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
||||
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_userManager = userManager;
|
||||
_crypto = cryptoProvider;
|
||||
}
|
||||
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
SerializablePasswordReset spr;
|
||||
HashSet<string> usersreset = new HashSet<string>();
|
||||
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
|
||||
{
|
||||
using (var str = File.OpenRead(resetfile))
|
||||
{
|
||||
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (spr.ExpirationDate < DateTime.Now)
|
||||
{
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
else if (spr.Pin.Replace('-', '').Equals(pin.Replace('-', ''), StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var resetUser = _userManager.GetUserByName(spr.UserName);
|
||||
if (resetUser == null)
|
||||
{
|
||||
throw new Exception($"User with a username of {spr.UserName} not found");
|
||||
}
|
||||
|
||||
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||
usersreset.Add(resetUser.Name);
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
}
|
||||
|
||||
if (usersreset.Count < 1)
|
||||
{
|
||||
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PinRedeemResult
|
||||
{
|
||||
Success = true,
|
||||
UsersReset = usersreset.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
|
||||
{
|
||||
string pin = string.Empty;
|
||||
using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
cryptoRandom.GetBytes(bytes);
|
||||
pin = BitConverter.ToString(bytes);
|
||||
}
|
||||
|
||||
DateTime expireTime = DateTime.Now.AddMinutes(30);
|
||||
string filePath = _passwordResetFileBase + user.InternalId + ".json";
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = filePath,
|
||||
UserName = user.Name
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (FileStream fileStream = File.OpenWrite(filePath))
|
||||
{
|
||||
_jsonSerializer.SerializeToStream(spr, fileStream);
|
||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e);
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.PinCode,
|
||||
PinExpirationDate = expireTime,
|
||||
PinFile = filePath
|
||||
};
|
||||
}
|
||||
|
||||
private class SerializablePasswordReset : PasswordPinCreationResult
|
||||
{
|
||||
public string Pin { get; set; }
|
||||
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,6 +79,9 @@ namespace Emby.Server.Implementations.Library
|
|||
private IAuthenticationProvider[] _authenticationProviders;
|
||||
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||
|
||||
private IPasswordResetProvider[] _passwordResetProviders;
|
||||
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||
|
||||
public UserManager(
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerConfigurationManager configurationManager,
|
||||
|
@ -102,8 +105,6 @@ namespace Emby.Server.Implementations.Library
|
|||
_fileSystem = fileSystem;
|
||||
ConfigurationManager = configurationManager;
|
||||
_users = Array.Empty<User>();
|
||||
|
||||
DeletePinFile();
|
||||
}
|
||||
|
||||
public NameIdPair[] GetAuthenticationProviders()
|
||||
|
@ -120,11 +121,29 @@ namespace Emby.Server.Implementations.Library
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders)
|
||||
public NameIdPair[] GetPasswordResetProviders()
|
||||
{
|
||||
return _passwordResetProviders
|
||||
.Where(i => i.IsEnabled)
|
||||
.OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
|
||||
.ThenBy(i => i.Name)
|
||||
.Select(i => new NameIdPair
|
||||
{
|
||||
Name = i.Name,
|
||||
Id = GetPasswordResetProviderId(i)
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders)
|
||||
{
|
||||
_authenticationProviders = authenticationProviders.ToArray();
|
||||
|
||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||
|
||||
_passwordResetProviders = passwordResetProviders.ToArray();
|
||||
|
||||
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||
}
|
||||
|
||||
#region UserUpdated Event
|
||||
|
@ -342,11 +361,21 @@ namespace Emby.Server.Implementations.Library
|
|||
return provider.GetType().FullName;
|
||||
}
|
||||
|
||||
private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
|
||||
{
|
||||
return provider.GetType().FullName;
|
||||
}
|
||||
|
||||
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
||||
{
|
||||
return GetAuthenticationProviders(user).First();
|
||||
}
|
||||
|
||||
private IPasswordResetProvider GetPasswordResetProvider(User user)
|
||||
{
|
||||
return GetPasswordResetProviders(user)[0];
|
||||
}
|
||||
|
||||
private IAuthenticationProvider[] GetAuthenticationProviders(User user)
|
||||
{
|
||||
var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
|
||||
|
@ -366,6 +395,25 @@ namespace Emby.Server.Implementations.Library
|
|||
return providers;
|
||||
}
|
||||
|
||||
private IPasswordResetProvider[] GetPasswordResetProviders(User user)
|
||||
{
|
||||
var passwordResetProviderId = user?.Policy.PasswordResetProviderId;
|
||||
|
||||
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
|
||||
|
||||
if (!string.IsNullOrEmpty(passwordResetProviderId))
|
||||
{
|
||||
providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
}
|
||||
|
||||
if (providers.Length == 0)
|
||||
{
|
||||
providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider };
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
||||
{
|
||||
try
|
||||
|
@ -844,159 +892,51 @@ namespace Emby.Server.Implementations.Library
|
|||
Id = Guid.NewGuid(),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
UsesIdForConfigurationPath = true,
|
||||
//Salt = BCrypt.GenerateSalt()
|
||||
UsesIdForConfigurationPath = true
|
||||
};
|
||||
}
|
||||
|
||||
private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt");
|
||||
|
||||
private string _lastPin;
|
||||
private PasswordPinCreationResult _lastPasswordPinCreationResult;
|
||||
private int _pinAttempts;
|
||||
|
||||
private async Task<PasswordPinCreationResult> CreatePasswordResetPin()
|
||||
{
|
||||
var num = new Random().Next(1, 9999);
|
||||
|
||||
var path = PasswordResetFile;
|
||||
|
||||
var pin = num.ToString("0000", CultureInfo.InvariantCulture);
|
||||
_lastPin = pin;
|
||||
|
||||
var time = TimeSpan.FromMinutes(5);
|
||||
var expiration = DateTime.UtcNow.Add(time);
|
||||
|
||||
var text = new StringBuilder();
|
||||
|
||||
var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty;
|
||||
|
||||
text.AppendLine("Use your web browser to visit:");
|
||||
text.AppendLine(string.Empty);
|
||||
text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html");
|
||||
text.AppendLine(string.Empty);
|
||||
text.AppendLine("Enter the following pin code:");
|
||||
text.AppendLine(string.Empty);
|
||||
text.AppendLine(pin);
|
||||
text.AppendLine(string.Empty);
|
||||
|
||||
var localExpirationTime = expiration.ToLocalTime();
|
||||
// Tuesday, 22 August 2006 06:30 AM
|
||||
text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture));
|
||||
|
||||
File.WriteAllText(path, text.ToString(), Encoding.UTF8);
|
||||
|
||||
var result = new PasswordPinCreationResult
|
||||
{
|
||||
PinFile = path,
|
||||
ExpirationDate = expiration
|
||||
};
|
||||
|
||||
_lastPasswordPinCreationResult = result;
|
||||
_pinAttempts = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
|
||||
{
|
||||
DeletePinFile();
|
||||
|
||||
var user = string.IsNullOrWhiteSpace(enteredUsername) ?
|
||||
null :
|
||||
GetUserByName(enteredUsername);
|
||||
|
||||
var action = ForgotPasswordAction.InNetworkRequired;
|
||||
string pinFile = null;
|
||||
DateTime? expirationDate = null;
|
||||
|
||||
if (user != null && !user.Policy.IsAdministrator)
|
||||
if (user != null && isInNetwork)
|
||||
{
|
||||
action = ForgotPasswordAction.ContactAdmin;
|
||||
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isInNetwork)
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
action = ForgotPasswordAction.PinCode;
|
||||
}
|
||||
|
||||
var result = await CreatePasswordResetPin().ConfigureAwait(false);
|
||||
pinFile = result.PinFile;
|
||||
expirationDate = result.ExpirationDate;
|
||||
Action = action,
|
||||
PinFile = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = action,
|
||||
PinFile = pinFile,
|
||||
PinExpirationDate = expirationDate
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
DeletePinFile();
|
||||
|
||||
var usersReset = new List<string>();
|
||||
|
||||
var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
|
||||
string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
|
||||
_lastPasswordPinCreationResult != null &&
|
||||
_lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
|
||||
|
||||
if (valid)
|
||||
foreach (var provider in _passwordResetProviders)
|
||||
{
|
||||
_lastPin = null;
|
||||
_lastPasswordPinCreationResult = null;
|
||||
|
||||
foreach (var user in Users)
|
||||
var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
|
||||
if (result.Success)
|
||||
{
|
||||
await ResetPassword(user).ConfigureAwait(false);
|
||||
|
||||
if (user.Policy.IsDisabled)
|
||||
{
|
||||
user.Policy.IsDisabled = false;
|
||||
UpdateUserPolicy(user, user.Policy, true);
|
||||
}
|
||||
usersReset.Add(user.Name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_pinAttempts++;
|
||||
if (_pinAttempts >= 3)
|
||||
{
|
||||
_lastPin = null;
|
||||
_lastPasswordPinCreationResult = null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return new PinRedeemResult
|
||||
{
|
||||
Success = valid,
|
||||
UsersReset = usersReset.ToArray()
|
||||
Success = false,
|
||||
UsersReset = Array.Empty<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private void DeletePinFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(PasswordResetFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordPinCreationResult
|
||||
{
|
||||
public string PinFile { get; set; }
|
||||
public DateTime ExpirationDate { get; set; }
|
||||
}
|
||||
|
||||
public UserPolicy GetUserPolicy(User user)
|
||||
{
|
||||
var path = GetPolicyFilePath(user);
|
||||
|
|
|
@ -245,6 +245,12 @@ namespace MediaBrowser.Api.Session
|
|||
{
|
||||
}
|
||||
|
||||
[Route("/Auth/PasswordResetProviders", "GET")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class GetPasswordResetProviders : IReturn<NameIdPair[]>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Auth/Keys/{Key}", "DELETE")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class RevokeKey
|
||||
|
@ -294,6 +300,11 @@ namespace MediaBrowser.Api.Session
|
|||
return _userManager.GetAuthenticationProviders();
|
||||
}
|
||||
|
||||
public object Get(GetPasswordResetProviders request)
|
||||
{
|
||||
return _userManager.GetPasswordResetProviders();
|
||||
}
|
||||
|
||||
public void Delete(RevokeKey request)
|
||||
{
|
||||
_sessionManager.RevokeToken(request.Key);
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace MediaBrowser.Controller.Authentication
|
||||
{
|
||||
public interface IPasswordResetProvider
|
||||
{
|
||||
string Name { get; }
|
||||
bool IsEnabled { get; }
|
||||
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
|
||||
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
|
||||
}
|
||||
public class PasswordPinCreationResult
|
||||
{
|
||||
public string PinFile { get; set; }
|
||||
public DateTime ExpirationDate { get; set; }
|
||||
}
|
||||
}
|
|
@ -200,8 +200,9 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <returns>System.String.</returns>
|
||||
string MakeValidUsername(string username);
|
||||
|
||||
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders);
|
||||
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
|
||||
|
||||
NameIdPair[] GetAuthenticationProviders();
|
||||
NameIdPair[] GetPasswordResetProviders();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ namespace MediaBrowser.Model.Users
|
|||
|
||||
public int RemoteClientBitrateLimit { get; set; }
|
||||
public string AuthenticationProviderId { get; set; }
|
||||
public string PasswordResetProviderId { get; set; }
|
||||
|
||||
public UserPolicy()
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue
Block a user