#pragma warning disable CA1307 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.Users { /// /// Manages the creation and retrieval of instances. /// public class UserManager : IUserManager { private readonly JellyfinDbProvider _dbProvider; private readonly IEventManager _eventManager; private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; private readonly ILogger _logger; private readonly IReadOnlyCollection _passwordResetProviders; private readonly IReadOnlyCollection _authenticationProviders; private readonly InvalidAuthProvider _invalidAuthProvider; private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly IDictionary _users; /// /// Initializes a new instance of the class. /// /// The database provider. /// The event manager. /// The cryptography provider. /// The network manager. /// The application host. /// The image processor. /// The logger. public UserManager( JellyfinDbProvider dbProvider, IEventManager eventManager, ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, ILogger logger) { _dbProvider = dbProvider; _eventManager = eventManager; _cryptoProvider = cryptoProvider; _networkManager = networkManager; _appHost = appHost; _imageProcessor = imageProcessor; _logger = logger; _passwordResetProviders = appHost.GetExports(); _authenticationProviders = appHost.GetExports(); _invalidAuthProvider = _authenticationProviders.OfType().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); _defaultPasswordResetProvider = _passwordResetProviders.OfType().First(); _users = new ConcurrentDictionary(); using var dbContext = _dbProvider.CreateContext(); foreach (var user in dbContext.Users .Include(user => user.Permissions) .Include(user => user.Preferences) .Include(user => user.AccessSchedules) .Include(user => user.ProfileImage) .AsEnumerable()) { _users.Add(user.Id, user); } } /// public event EventHandler>? OnUserUpdated; /// public IEnumerable Users => _users.Values; /// public IEnumerable UsersIds => _users.Keys; /// public User? GetUserById(Guid id) { if (id.Equals(default)) { throw new ArgumentException("Guid can't be empty", nameof(id)); } _users.TryGetValue(id, out var user); return user; } /// public User? GetUserByName(string name) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Invalid username", nameof(name)); } return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); } /// public async Task RenameUser(User user, string newName) { if (user == null) { throw new ArgumentNullException(nameof(user)); } ThrowIfInvalidUsername(newName); if (user.Username.Equals(newName, StringComparison.Ordinal)) { throw new ArgumentException("The new and old names must be different."); } await using var dbContext = _dbProvider.CreateContext(); if (await dbContext.Users .AsQueryable() .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id)) .ConfigureAwait(false)) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, "A user with the name '{0}' already exists.", newName)); } user.Username = newName; await UpdateUserAsync(user).ConfigureAwait(false); OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); } /// public async Task UpdateUserAsync(User user) { await using var dbContext = _dbProvider.CreateContext(); dbContext.Users.Update(user); _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } internal async Task CreateUserInternalAsync(string name, JellyfinDb dbContext) { // TODO: Remove after user item data is migrated. var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false) ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false) : 0; var user = new User( name, _defaultAuthenticationProvider.GetType().FullName!, _defaultPasswordResetProvider.GetType().FullName!) { InternalId = max + 1 }; user.AddDefaultPermissions(); user.AddDefaultPreferences(); _users.Add(user.Id, user); return user; } /// public async Task CreateUserAsync(string name) { ThrowIfInvalidUsername(name); if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, "A user with the name '{0}' already exists.", name)); } await using var dbContext = _dbProvider.CreateContext(); var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync().ConfigureAwait(false); await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false); return newUser; } /// public async Task DeleteUserAsync(Guid userId) { if (!_users.TryGetValue(userId, out var user)) { throw new ResourceNotFoundException(nameof(userId)); } if (_users.Count == 1) { throw new InvalidOperationException(string.Format( CultureInfo.InvariantCulture, "The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Username)); } if (user.HasPermission(PermissionKind.IsAdministrator) && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) { throw new ArgumentException( string.Format( CultureInfo.InvariantCulture, "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Username), nameof(userId)); } await using var dbContext = _dbProvider.CreateContext(); dbContext.Users.Remove(user); await dbContext.SaveChangesAsync().ConfigureAwait(false); _users.Remove(userId); await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); } /// public Task ResetPassword(User user) { return ChangePassword(user, string.Empty); } /// public Task ResetEasyPassword(User user) { return ChangeEasyPassword(user, string.Empty, null); } /// public async Task ChangePassword(User user, string newPassword) { if (user == null) { throw new ArgumentNullException(nameof(user)); } await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false); await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); } /// public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1) { if (newPassword != null) { newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString(); } if (string.IsNullOrWhiteSpace(newPasswordSha1)) { throw new ArgumentNullException(nameof(newPasswordSha1)); } user.EasyPassword = newPasswordSha1; await UpdateUserAsync(user).ConfigureAwait(false); _eventManager.Publish(new UserPasswordChangedEventArgs(user)); } /// public UserDto GetUserDto(User user, string? remoteEndPoint = null) { var hasPassword = GetAuthenticationProvider(user).HasPassword(user); return new UserDto { Name = user.Username, Id = user.Id, ServerId = _appHost.SystemId, HasPassword = hasPassword, HasConfiguredPassword = hasPassword, HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword), EnableAutoLogin = user.EnableAutoLogin, LastLoginDate = user.LastLoginDate, LastActivityDate = user.LastActivityDate, PrimaryImageTag = user.ProfileImage != null ? _imageProcessor.GetImageCacheTag(user) : null, Configuration = new UserConfiguration { SubtitleMode = user.SubtitleMode, HidePlayedInLatest = user.HidePlayedInLatest, EnableLocalPassword = user.EnableLocalPassword, PlayDefaultAudioTrack = user.PlayDefaultAudioTrack, DisplayCollectionsView = user.DisplayCollectionsView, DisplayMissingEpisodes = user.DisplayMissingEpisodes, AudioLanguagePreference = user.AudioLanguagePreference, RememberAudioSelections = user.RememberAudioSelections, EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, RememberSubtitleSelections = user.RememberSubtitleSelections, SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty, OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) }, Policy = new UserPolicy { MaxParentalRating = user.MaxParentalAgeRating, EnableUserPreferenceAccess = user.EnableUserPreferenceAccess, RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0, AuthenticationProviderId = user.AuthenticationProviderId, PasswordResetProviderId = user.PasswordResetProviderId, InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1, MaxActiveSessions = user.MaxActiveSessions, IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), IsHidden = user.HasPermission(PermissionKind.IsHidden), IsDisabled = user.HasPermission(PermissionKind.IsDisabled), EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl), EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess), EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement), EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess), EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback), EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding), EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion), EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading), EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding), EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion), EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels), EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices), EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders), EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers), EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), AccessSchedules = user.AccessSchedules.ToArray(), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), EnabledChannels = user.GetPreferenceValues(PreferenceKind.EnabledChannels), EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), EnabledFolders = user.GetPreferenceValues(PreferenceKind.EnabledFolders), EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders), SyncPlayAccess = user.SyncPlayAccess, BlockedChannels = user.GetPreferenceValues(PreferenceKind.BlockedChannels), BlockedMediaFolders = user.GetPreferenceValues(PreferenceKind.BlockedMediaFolders), BlockUnratedItems = user.GetPreferenceValues(PreferenceKind.BlockUnratedItems) } }; } /// public async Task AuthenticateUser( string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession) { if (string.IsNullOrWhiteSpace(username)) { _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); throw new ArgumentNullException(nameof(username)); } var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) .ConfigureAwait(false); var authenticationProvider = authResult.AuthenticationProvider; var success = authResult.Success; if (user == null) { string updatedUsername = authResult.Username; if (success && authenticationProvider != null && authenticationProvider is not DefaultAuthenticationProvider) { // Trust the username returned by the authentication provider username = updatedUsername; // Search the database for the user again // the authentication provider might have created it user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user != null) { await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false); } } } if (success && user != null && authenticationProvider != null) { var providerId = authenticationProvider.GetType().FullName; if (providerId != null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) { user.AuthenticationProviderId = providerId; await UpdateUserAsync(user).ConfigureAwait(false); } } if (user == null) { _logger.LogInformation( "Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint); throw new AuthenticationException("Invalid username or password entered."); } if (user.HasPermission(PermissionKind.IsDisabled)) { _logger.LogInformation( "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint); throw new SecurityException( $"The {user.Username} account is currently disabled. Please consult with your administrator."); } if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(remoteEndPoint)) { _logger.LogInformation( "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint); throw new SecurityException("Forbidden."); } if (!user.IsParentalScheduleAllowed()) { _logger.LogInformation( "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint); throw new SecurityException("User is not allowed access at this time."); } // Update LastActivityDate and LastLoginDate, then save if (success) { if (isUserSession) { user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; } user.InvalidLoginAttemptCount = 0; await UpdateUserAsync(user).ConfigureAwait(false); _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); } else { await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false); _logger.LogInformation( "Authentication request for {UserName} has been denied (IP: {IP}).", user.Username, remoteEndPoint); } return success ? user : null; } /// public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) { var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); if (user != null && isInNetwork) { var passwordResetProvider = GetPasswordResetProvider(user); var result = await passwordResetProvider .StartForgotPasswordProcess(user, isInNetwork) .ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false); return result; } return new ForgotPasswordResult { Action = ForgotPasswordAction.InNetworkRequired, PinFile = string.Empty }; } /// public async Task RedeemPasswordResetPin(string pin) { foreach (var provider in _passwordResetProviders) { var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); if (result.Success) { return result; } } return new PinRedeemResult(); } /// public async Task InitializeAsync() { // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. if (_users.Any()) { return; } var defaultName = Environment.UserName; if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName)) { defaultName = "MyJellyfinUser"; } _logger.LogWarning("No users, creating one with username {UserName}", defaultName); await using var dbContext = _dbProvider.CreateContext(); var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true); dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync().ConfigureAwait(false); } /// public NameIdPair[] GetAuthenticationProviders() { return _authenticationProviders .Where(provider => provider.IsEnabled) .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) .ThenBy(i => i.Name) .Select(i => new NameIdPair { Name = i.Name, Id = i.GetType().FullName }) .ToArray(); } /// public NameIdPair[] GetPasswordResetProviders() { return _passwordResetProviders .Where(provider => provider.IsEnabled) .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) .ThenBy(i => i.Name) .Select(i => new NameIdPair { Name = i.Name, Id = i.GetType().FullName }) .ToArray(); } /// public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config) { await using var dbContext = _dbProvider.CreateContext(); var user = dbContext.Users .Include(u => u.Permissions) .Include(u => u.Preferences) .Include(u => u.AccessSchedules) .Include(u => u.ProfileImage) .FirstOrDefault(u => u.Id.Equals(userId)) ?? throw new ArgumentException("No user exists with given Id!"); user.SubtitleMode = config.SubtitleMode; user.HidePlayedInLatest = config.HidePlayedInLatest; user.EnableLocalPassword = config.EnableLocalPassword; user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; user.DisplayCollectionsView = config.DisplayCollectionsView; user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; user.AudioLanguagePreference = config.AudioLanguagePreference; user.RememberAudioSelections = config.RememberAudioSelections; user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; user.RememberSubtitleSelections = config.RememberSubtitleSelections; user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); dbContext.Update(user); _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } /// public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy) { await using var dbContext = _dbProvider.CreateContext(); var user = dbContext.Users .Include(u => u.Permissions) .Include(u => u.Preferences) .Include(u => u.AccessSchedules) .Include(u => u.ProfileImage) .FirstOrDefault(u => u.Id.Equals(userId)) ?? throw new ArgumentException("No user exists with given Id!"); // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch { -1 => null, 0 => 3, _ => policy.LoginAttemptsBeforeLockout }; user.MaxParentalAgeRating = policy.MaxParentalRating; user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; user.AuthenticationProviderId = policy.AuthenticationProviderId; user.PasswordResetProviderId = policy.PasswordResetProviderId; user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; user.LoginAttemptsBeforeLockout = maxLoginAttempts; user.MaxActiveSessions = policy.MaxActiveSessions; user.SyncPlayAccess = policy.SyncPlayAccess; user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); user.AccessSchedules.Clear(); foreach (var policyAccessSchedule in policy.AccessSchedules) { user.AccessSchedules.Add(policyAccessSchedule); } // TODO: fix this at some point user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty()); user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); dbContext.Update(user); _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } /// public async Task ClearProfileImageAsync(User user) { if (user.ProfileImage == null) { return; } await using var dbContext = _dbProvider.CreateContext(); dbContext.Remove(user.ProfileImage); await dbContext.SaveChangesAsync().ConfigureAwait(false); user.ProfileImage = null; _users[user.Id] = user; } internal static void ThrowIfInvalidUsername(string name) { if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name)) { return; } throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name)); } private static bool IsValidUsername(string name) { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) return Regex.IsMatch(name, @"^[\w\ \-'._@]+$"); } private IAuthenticationProvider GetAuthenticationProvider(User user) { return GetAuthenticationProviders(user)[0]; } private IPasswordResetProvider GetPasswordResetProvider(User user) { return GetPasswordResetProviders(user)[0]; } private IList GetAuthenticationProviders(User? user) { var authenticationProviderId = user?.AuthenticationProviderId; var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList(); if (!string.IsNullOrEmpty(authenticationProviderId)) { providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList(); } if (providers.Count == 0) { // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found _logger.LogWarning( "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user?.Username, user?.AuthenticationProviderId); providers = new List { _invalidAuthProvider }; } return providers; } private IList GetPasswordResetProviders(User user) { var passwordResetProviderId = user.PasswordResetProviderId; var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); if (!string.IsNullOrEmpty(passwordResetProviderId)) { providers = providers.Where(i => string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)) .ToArray(); } if (providers.Length == 0) { providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider }; } return providers; } private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser( string username, string password, User? user, string remoteEndPoint) { bool success = false; IAuthenticationProvider? authenticationProvider = null; foreach (var provider in GetAuthenticationProviders(user)) { var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); var updatedUsername = providerAuthResult.Username; success = providerAuthResult.Success; if (success) { authenticationProvider = provider; username = updatedUsername; break; } } if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user?.EnableLocalPassword == true && !string.IsNullOrEmpty(user.EasyPassword)) { // Check easy password var passwordHash = PasswordHash.Parse(user.EasyPassword); success = _cryptoProvider.Verify(passwordHash, password); } return (authenticationProvider, username, success); } private async Task<(string Username, bool Success)> AuthenticateWithProvider( IAuthenticationProvider provider, string username, string password, User? resolvedUser) { try { var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) : await provider.Authenticate(username, password).ConfigureAwait(false); if (authenticationResult.Username != username) { _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); username = authenticationResult.Username; } return (username, true); } catch (AuthenticationException ex) { _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); return (username, false); } } private async Task IncrementInvalidLoginAttemptCount(User user) { user.InvalidLoginAttemptCount++; int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins) { user.SetPermission(PermissionKind.IsDisabled, true); await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false); _logger.LogWarning( "Disabling user {Username} due to {Attempts} unsuccessful login attempts.", user.Username, user.InvalidLoginAttemptCount); } await UpdateUserAsync(user).ConfigureAwait(false); } } }