Browse Source

Merge pull request #3357 from crobibero/api-authorization

Add Authorization handlers
David 5 years ago
parent
commit
522e44de59
32 changed files with 951 additions and 171 deletions
  1. 25 20
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  2. 78 37
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  3. 102 0
      Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
  4. 14 11
      Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
  5. 42 0
      Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
  6. 11 0
      Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
  7. 18 4
      Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
  8. 42 0
      Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
  9. 11 0
      Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
  10. 44 0
      Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
  11. 11 0
      Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
  12. 24 2
      Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
  13. 38 0
      Jellyfin.Api/Constants/InternalClaimTypes.cs
  14. 15 0
      Jellyfin.Api/Constants/Policies.cs
  15. 1 1
      Jellyfin.Api/Controllers/ConfigurationController.cs
  16. 1 1
      Jellyfin.Api/Controllers/DevicesController.cs
  17. 1 1
      Jellyfin.Api/Controllers/PackageController.cs
  18. 2 1
      Jellyfin.Api/Controllers/SearchController.cs
  19. 4 4
      Jellyfin.Api/Controllers/SubtitleController.cs
  20. 2 1
      Jellyfin.Api/Controllers/VideoAttachmentsController.cs
  21. 77 0
      Jellyfin.Api/Helpers/ClaimHelpers.cs
  22. 35 2
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  23. 23 2
      MediaBrowser.Controller/Net/IAuthService.cs
  24. 11 0
      MediaBrowser.Controller/Net/IAuthorizationContext.cs
  25. 21 50
      tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
  26. 53 0
      tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
  27. 20 26
      tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
  28. 62 0
      tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
  29. 58 0
      tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
  30. 23 8
      tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
  31. 1 0
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  32. 81 0
      tests/Jellyfin.Api.Tests/TestHelpers.cs

+ 25 - 20
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _networkManager = networkManager;
         }
 
-        public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
+        public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
         {
-            ValidateUser(request, authAttribtues);
+            ValidateUser(request, authAttributes);
         }
 
         public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return user;
         }
 
-        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
+        public AuthorizationInfo Authenticate(HttpRequest request)
+        {
+            var auth = _authorizationContext.GetAuthorizationInfo(request);
+            if (auth?.User == null)
+            {
+                return null;
+            }
+
+            if (auth.User.HasPermission(PermissionKind.IsDisabled))
+            {
+                throw new SecurityException("User account has been disabled.");
+            }
+
+            return auth;
+        }
+
+        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
         {
             // This code is executed before the service
             var auth = _authorizationContext.GetAuthorizationInfo(request);
 
-            if (!IsExemptFromAuthenticationToken(authAttribtues, request))
+            if (!IsExemptFromAuthenticationToken(authAttributes, request))
             {
                 ValidateSecurityToken(request, auth.Token);
             }
 
-            if (authAttribtues.AllowLocalOnly && !request.IsLocal)
+            if (authAttributes.AllowLocalOnly && !request.IsLocal)
             {
                 throw new SecurityException("Operation not found.");
             }
@@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (user != null)
             {
-                ValidateUserAccess(user, request, authAttribtues, auth);
+                ValidateUserAccess(user, request, authAttributes);
             }
 
             var info = GetTokenInfo(request);
 
-            if (!IsExemptFromRoles(auth, authAttribtues, request, info))
+            if (!IsExemptFromRoles(auth, authAttributes, request, info))
             {
-                var roles = authAttribtues.GetRoles();
+                var roles = authAttributes.GetRoles();
 
                 ValidateRoles(roles, user);
             }
@@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private void ValidateUserAccess(
             User user,
             IRequest request,
-            IAuthenticationAttributes authAttributes,
-            AuthorizationInfo auth)
+            IAuthenticationAttributes authAttributes)
         {
             if (user.HasPermission(PermissionKind.IsDisabled))
             {
@@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 throw new AuthenticationException("Access token is invalid or expired.");
             }
-
-            //if (!string.IsNullOrEmpty(info.UserId))
-            //{
-            //    var user = _userManager.GetUserById(info.UserId);
-
-            //    if (user == null || user.Configuration.IsDisabled)
-            //    {
-            //        throw new SecurityException("User account has been disabled.");
-            //    }
-            //}
         }
     }
 }

+ 78 - 37
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 using Microsoft.Net.Http.Headers;
 
 namespace Emby.Server.Implementations.HttpServer.Security
@@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return GetAuthorization(requestContext);
         }
 
+        public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
+        {
+            var auth = GetAuthorizationDictionary(requestContext);
+            var (authInfo, _) =
+                GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+            return authInfo;
+        }
+
         /// <summary>
         /// Gets the authorization.
         /// </summary>
@@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private AuthorizationInfo GetAuthorization(IRequest httpReq)
         {
             var auth = GetAuthorizationDictionary(httpReq);
+            var (authInfo, originalAuthInfo) =
+                GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
 
+            if (originalAuthInfo != null)
+            {
+                httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+            }
+
+            httpReq.Items["AuthorizationInfo"] = authInfo;
+            return authInfo;
+        }
+
+        private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+            in Dictionary<string, string> auth,
+            in IHeaderDictionary headers,
+            in IQueryCollection queryString)
+        {
             string deviceId = null;
             string device = null;
             string client = null;
@@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.Headers["X-Emby-Token"];
+                token = headers["X-Emby-Token"];
             }
 
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.Headers["X-MediaBrowser-Token"];
+                token = headers["X-MediaBrowser-Token"];
             }
+
+            if (string.IsNullOrEmpty(token))
+            {
+                token = queryString["ApiKey"];
+            }
+
+            // TODO deprecate this query parameter.
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.QueryString["api_key"];
+                token = queryString["api_key"];
             }
 
-            var info = new AuthorizationInfo
+            var authInfo = new AuthorizationInfo
             {
                 Client = client,
                 Device = device,
@@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 Token = token
             };
 
+            AuthenticationInfo originalAuthenticationInfo = null;
             if (!string.IsNullOrWhiteSpace(token))
             {
                 var result = _authRepo.Get(new AuthenticationInfoQuery
@@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
                     AccessToken = token
                 });
 
-                var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
+                originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
 
-                if (tokenInfo != null)
+                if (originalAuthenticationInfo != null)
                 {
                     var updateToken = false;
 
                     // TODO: Remove these checks for IsNullOrWhiteSpace
-                    if (string.IsNullOrWhiteSpace(info.Client))
+                    if (string.IsNullOrWhiteSpace(authInfo.Client))
                     {
-                        info.Client = tokenInfo.AppName;
+                        authInfo.Client = originalAuthenticationInfo.AppName;
                     }
 
-                    if (string.IsNullOrWhiteSpace(info.DeviceId))
+                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
                     {
-                        info.DeviceId = tokenInfo.DeviceId;
+                        authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
                     }
 
                     // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
-                    var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+                    var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
 
-                    if (string.IsNullOrWhiteSpace(info.Device))
+                    if (string.IsNullOrWhiteSpace(authInfo.Device))
                     {
-                        info.Device = tokenInfo.DeviceName;
+                        authInfo.Device = originalAuthenticationInfo.DeviceName;
                     }
-
-                    else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+                    else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
                     {
                         if (allowTokenInfoUpdate)
                         {
                             updateToken = true;
-                            tokenInfo.DeviceName = info.Device;
+                            originalAuthenticationInfo.DeviceName = authInfo.Device;
                         }
                     }
 
-                    if (string.IsNullOrWhiteSpace(info.Version))
+                    if (string.IsNullOrWhiteSpace(authInfo.Version))
                     {
-                        info.Version = tokenInfo.AppVersion;
+                        authInfo.Version = originalAuthenticationInfo.AppVersion;
                     }
-                    else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+                    else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
                     {
                         if (allowTokenInfoUpdate)
                         {
                             updateToken = true;
-                            tokenInfo.AppVersion = info.Version;
+                            originalAuthenticationInfo.AppVersion = authInfo.Version;
                         }
                     }
 
-                    if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
+                    if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
                     {
-                        tokenInfo.DateLastActivity = DateTime.UtcNow;
+                        originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
                         updateToken = true;
                     }
 
-                    if (!tokenInfo.UserId.Equals(Guid.Empty))
+                    if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
                     {
-                        info.User = _userManager.GetUserById(tokenInfo.UserId);
+                        authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
 
-                        if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
+                        if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
                         {
-                            tokenInfo.UserName = info.User.Username;
+                            originalAuthenticationInfo.UserName = authInfo.User.Username;
                             updateToken = true;
                         }
                     }
 
                     if (updateToken)
                     {
-                        _authRepo.Update(tokenInfo);
+                        _authRepo.Update(originalAuthenticationInfo);
                     }
                 }
-                httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
             }
 
-            httpReq.Items["AuthorizationInfo"] = info;
-
-            return info;
+            return (authInfo, originalAuthenticationInfo);
         }
 
         /// <summary>
@@ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return GetAuthorization(auth);
         }
 
+        /// <summary>
+        /// Gets the auth.
+        /// </summary>
+        /// <param name="httpReq">The HTTP req.</param>
+        /// <returns>Dictionary{System.StringSystem.String}.</returns>
+        private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
+        {
+            var auth = httpReq.Headers["X-Emby-Authorization"];
+
+            if (string.IsNullOrEmpty(auth))
+            {
+                auth = httpReq.Headers[HeaderNames.Authorization];
+            }
+
+            return GetAuthorization(auth);
+        }
+
         /// <summary>
         /// Gets the authorization.
         /// </summary>
@@ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
         private static string NormalizeValue(string value)
         {
-            if (string.IsNullOrEmpty(value))
-            {
-                return value;
-            }
-
-            return WebUtility.HtmlEncode(value);
+            return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
         }
     }
 }

+ 102 - 0
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs

@@ -0,0 +1,102 @@
+#nullable enable
+
+using System.Net;
+using System.Security.Claims;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth
+{
+    /// <summary>
+    /// Base authorization handler.
+    /// </summary>
+    /// <typeparam name="T">Type of Authorization Requirement.</typeparam>
+    public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
+        where T : IAuthorizationRequirement
+    {
+        private readonly IUserManager _userManager;
+        private readonly INetworkManager _networkManager;
+        private readonly IHttpContextAccessor _httpContextAccessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        protected BaseAuthorizationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+        {
+            _userManager = userManager;
+            _networkManager = networkManager;
+            _httpContextAccessor = httpContextAccessor;
+        }
+
+        /// <summary>
+        /// Validate authenticated claims.
+        /// </summary>
+        /// <param name="claimsPrincipal">Request claims.</param>
+        /// <param name="ignoreSchedule">Whether to ignore parental control.</param>
+        /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
+        /// <returns>Validated claim status.</returns>
+        protected bool ValidateClaims(
+            ClaimsPrincipal claimsPrincipal,
+            bool ignoreSchedule = false,
+            bool localAccessOnly = false)
+        {
+            // Ensure claim has userId.
+            var userId = ClaimHelpers.GetUserId(claimsPrincipal);
+            if (userId == null)
+            {
+                return false;
+            }
+
+            // Ensure userId links to a valid user.
+            var user = _userManager.GetUserById(userId.Value);
+            if (user == null)
+            {
+                return false;
+            }
+
+            // Ensure user is not disabled.
+            if (user.HasPermission(PermissionKind.IsDisabled))
+            {
+                return false;
+            }
+
+            var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
+            var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
+            // User cannot access remotely and user is remote
+            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
+            {
+                return false;
+            }
+
+            if (localAccessOnly && !isInLocalNetwork)
+            {
+                return false;
+            }
+
+            // User attempting to access out of parental control hours.
+            if (!ignoreSchedule
+                && !user.HasPermission(PermissionKind.IsAdministrator)
+                && !user.IsParentalScheduleAllowed())
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        private static IPAddress NormalizeIp(IPAddress ip)
+        {
+            return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
+        }
+    }
+}

+ 14 - 11
Jellyfin.Api/Auth/CustomAuthenticationHandler.cs

@@ -1,3 +1,6 @@
+#nullable enable
+
+using System.Globalization;
 using System.Security.Authentication;
 using System.Security.Claims;
 using System.Text.Encodings.Web;
@@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth
         /// <inheritdoc />
         protected override Task<AuthenticateResult> HandleAuthenticateAsync()
         {
-            var authenticatedAttribute = new AuthenticatedAttribute
-            {
-                IgnoreLegacyAuth = true
-            };
-
             try
             {
-                var user = _authService.Authenticate(Request, authenticatedAttribute);
-                if (user == null)
+                var authorizationInfo = _authService.Authenticate(Request);
+                if (authorizationInfo == null)
                 {
                     return Task.FromResult(AuthenticateResult.NoResult());
                     // TODO return when legacy API is removed.
@@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth
 
                 var claims = new[]
                 {
-                    new Claim(ClaimTypes.Name, user.Username),
-                    new Claim(
-                        ClaimTypes.Role,
-                        value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
+                    new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
+                    new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
+                    new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
+                    new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
+                    new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
+                    new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
+                    new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
+                    new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
                 };
+
                 var identity = new ClaimsIdentity(claims, Scheme.Name);
                 var principal = new ClaimsPrincipal(identity);
                 var ticket = new AuthenticationTicket(principal, Scheme.Name);

+ 42 - 0
Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs

@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
+{
+    /// <summary>
+    /// Default authorization handler.
+    /// </summary>
+    public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public DefaultAuthorizationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User);
+            if (!validated)
+            {
+                context.Fail();
+                return Task.CompletedTask;
+            }
+
+            context.Succeed(requirement);
+            return Task.CompletedTask;
+        }
+    }
+}

+ 11 - 0
Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs

@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
+{
+    /// <summary>
+    /// The default authorization requirement.
+    /// </summary>
+    public class DefaultAuthorizationRequirement : IAuthorizationRequirement
+    {
+    }
+}

+ 18 - 4
Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs

@@ -1,22 +1,33 @@
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
 {
     /// <summary>
     /// Authorization handler for requiring first time setup or elevated privileges.
     /// </summary>
-    public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
+    public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
     {
         private readonly IConfigurationManager _configurationManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
         /// </summary>
-        /// <param name="configurationManager">The jellyfin configuration manager.</param>
-        public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
+        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public FirstTimeSetupOrElevatedHandler(
+            IConfigurationManager configurationManager,
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
         {
             _configurationManager = configurationManager;
         }
@@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
             if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
             {
                 context.Succeed(firstTimeSetupOrElevatedRequirement);
+                return Task.CompletedTask;
             }
-            else if (context.User.IsInRole(UserRoles.Administrator))
+
+            var validated = ValidateClaims(context.User);
+            if (validated && context.User.IsInRole(UserRoles.Administrator))
             {
                 context.Succeed(firstTimeSetupOrElevatedRequirement);
             }

+ 42 - 0
Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs

@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+{
+    /// <summary>
+    /// Escape schedule controls handler.
+    /// </summary>
+    public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public IgnoreScheduleHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, ignoreSchedule: true);
+            if (!validated)
+            {
+                context.Fail();
+                return Task.CompletedTask;
+            }
+
+            context.Succeed(requirement);
+            return Task.CompletedTask;
+        }
+    }
+}

+ 11 - 0
Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs

@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+{
+    /// <summary>
+    /// Escape schedule controls requirement.
+    /// </summary>
+    public class IgnoreScheduleRequirement : IAuthorizationRequirement
+    {
+    }
+}

+ 44 - 0
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs

@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.LocalAccessPolicy
+{
+    /// <summary>
+    /// Local access handler.
+    /// </summary>
+    public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public LocalAccessHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, localAccessOnly: true);
+            if (!validated)
+            {
+                context.Fail();
+            }
+            else
+            {
+                context.Succeed(requirement);
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}

+ 11 - 0
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs

@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LocalAccessPolicy
+{
+    /// <summary>
+    /// The local access authorization requirement.
+    /// </summary>
+    public class LocalAccessRequirement : IAuthorizationRequirement
+    {
+    }
+}

+ 24 - 2
Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs

@@ -1,21 +1,43 @@
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Auth.RequiresElevationPolicy
 {
     /// <summary>
     /// Authorization handler for requiring elevated privileges.
     /// </summary>
-    public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
+    public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public RequiresElevationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
         /// <inheritdoc />
         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
         {
-            if (context.User.IsInRole(UserRoles.Administrator))
+            var validated = ValidateClaims(context.User);
+            if (validated && context.User.IsInRole(UserRoles.Administrator))
             {
                 context.Succeed(requirement);
             }
+            else
+            {
+                context.Fail();
+            }
 
             return Task.CompletedTask;
         }

+ 38 - 0
Jellyfin.Api/Constants/InternalClaimTypes.cs

@@ -0,0 +1,38 @@
+namespace Jellyfin.Api.Constants
+{
+    /// <summary>
+    /// Internal claim types for authorization.
+    /// </summary>
+    public static class InternalClaimTypes
+    {
+        /// <summary>
+        /// User Id.
+        /// </summary>
+        public const string UserId = "Jellyfin-UserId";
+
+        /// <summary>
+        /// Device Id.
+        /// </summary>
+        public const string DeviceId = "Jellyfin-DeviceId";
+
+        /// <summary>
+        /// Device.
+        /// </summary>
+        public const string Device = "Jellyfin-Device";
+
+        /// <summary>
+        /// Client.
+        /// </summary>
+        public const string Client = "Jellyfin-Client";
+
+        /// <summary>
+        /// Version.
+        /// </summary>
+        public const string Version = "Jellyfin-Version";
+
+        /// <summary>
+        /// Token.
+        /// </summary>
+        public const string Token = "Jellyfin-Token";
+    }
+}

+ 15 - 0
Jellyfin.Api/Constants/Policies.cs

@@ -5,6 +5,11 @@ namespace Jellyfin.Api.Constants
     /// </summary>
     public static class Policies
     {
+        /// <summary>
+        /// Policy name for default authorization.
+        /// </summary>
+        public const string DefaultAuthorization = "DefaultAuthorization";
+
         /// <summary>
         /// Policy name for requiring first time setup or elevated privileges.
         /// </summary>
@@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
         /// Policy name for requiring elevated privileges.
         /// </summary>
         public const string RequiresElevation = "RequiresElevation";
+
+        /// <summary>
+        /// Policy name for allowing local access only.
+        /// </summary>
+        public const string LocalAccessOnly = "LocalAccessOnly";
+
+        /// <summary>
+        /// Policy name for escaping schedule controls.
+        /// </summary>
+        public const string IgnoreSchedule = "IgnoreSchedule";
     }
 }

+ 1 - 1
Jellyfin.Api/Controllers/ConfigurationController.cs

@@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// Configuration Controller.
     /// </summary>
     [Route("System")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ConfigurationController : BaseJellyfinApiController
     {
         private readonly IServerConfigurationManager _configurationManager;

+ 1 - 1
Jellyfin.Api/Controllers/DevicesController.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Devices Controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class DevicesController : BaseJellyfinApiController
     {
         private readonly IDeviceManager _deviceManager;

+ 1 - 1
Jellyfin.Api/Controllers/PackageController.cs

@@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// Package Controller.
     /// </summary>
     [Route("Packages")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PackageController : BaseJellyfinApiController
     {
         private readonly IInstallationManager _installationManager;

+ 2 - 1
Jellyfin.Api/Controllers/SearchController.cs

@@ -3,6 +3,7 @@ using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
@@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
     /// Search controller.
     /// </summary>
     [Route("/Search/Hints")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class SearchController : BaseJellyfinApiController
     {
         private readonly ISearchEngine _searchEngine;

+ 4 - 4
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -109,7 +109,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Subtitles retrieved.</response>
         /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
         [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
             [FromRoute] Guid id,
@@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Subtitle downloaded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DownloadRemoteSubtitles(
             [FromRoute] Guid id,
@@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
         [HttpGet("/Providers/Subtitles/Subtitles/{id}")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Produces(MediaTypeNames.Application.Octet)]
         public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
@@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Subtitle playlist retrieved.</response>
         /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
         [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitlePlaylist(
             [FromRoute] Guid id,

+ 2 - 1
Jellyfin.Api/Controllers/VideoAttachmentsController.cs

@@ -2,6 +2,7 @@ using System;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
@@ -15,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// Attachments controller.
     /// </summary>
     [Route("Videos")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class VideoAttachmentsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;

+ 77 - 0
Jellyfin.Api/Helpers/ClaimHelpers.cs

@@ -0,0 +1,77 @@
+#nullable enable
+
+using System;
+using System.Linq;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Claim Helpers.
+    /// </summary>
+    public static class ClaimHelpers
+    {
+        /// <summary>
+        /// Get user id from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>User id.</returns>
+        public static Guid? GetUserId(in ClaimsPrincipal user)
+        {
+            var value = GetClaimValue(user, InternalClaimTypes.UserId);
+            return string.IsNullOrEmpty(value)
+                ? null
+                : (Guid?)Guid.Parse(value);
+        }
+
+        /// <summary>
+        /// Get device id from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Device id.</returns>
+        public static string? GetDeviceId(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.DeviceId);
+
+        /// <summary>
+        /// Get device from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Device.</returns>
+        public static string? GetDevice(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Device);
+
+        /// <summary>
+        /// Get client from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Client.</returns>
+        public static string? GetClient(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Client);
+
+        /// <summary>
+        /// Get version from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Version.</returns>
+        public static string? GetVersion(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Version);
+
+        /// <summary>
+        /// Get token from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Token.</returns>
+        public static string? GetToken(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Token);
+
+        private static string? GetClaimValue(in ClaimsPrincipal user, string name)
+        {
+            return user?.Identities
+                .SelectMany(c => c.Claims)
+                .Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase))
+                .Select(claim => claim.Value)
+                .FirstOrDefault();
+        }
+    }
+}

+ 35 - 2
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -5,7 +5,10 @@ using System.Linq;
 using System.Reflection;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
@@ -15,6 +18,8 @@ using MediaBrowser.Common.Json;
 using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.HttpOverrides;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.OpenApi.Models;
 using Swashbuckle.AspNetCore.SwaggerGen;
@@ -33,16 +38,19 @@ namespace Jellyfin.Server.Extensions
         /// <returns>The updated service collection.</returns>
         public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
         {
+            serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
             return serviceCollection.AddAuthorizationCore(options =>
             {
                 options.AddPolicy(
-                    Policies.RequiresElevation,
+                    Policies.DefaultAuthorization,
                     policy =>
                     {
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
-                        policy.AddRequirements(new RequiresElevationRequirement());
+                        policy.AddRequirements(new DefaultAuthorizationRequirement());
                     });
                 options.AddPolicy(
                     Policies.FirstTimeSetupOrElevated,
@@ -51,6 +59,27 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
                     });
+                options.AddPolicy(
+                    Policies.IgnoreSchedule,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new IgnoreScheduleRequirement());
+                    });
+                options.AddPolicy(
+                    Policies.LocalAccessOnly,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new LocalAccessRequirement());
+                    });
+                options.AddPolicy(
+                    Policies.RequiresElevation,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new RequiresElevationRequirement());
+                    });
             });
         }
 
@@ -78,6 +107,10 @@ namespace Jellyfin.Server.Extensions
                 {
                     options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
                 })
+                .Configure<ForwardedHeadersOptions>(options =>
+                {
+                    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+                })
                 .AddMvc(opts =>
                 {
                     opts.UseGeneralRoutePrefix(baseUrl);

+ 23 - 2
MediaBrowser.Controller/Net/IAuthService.cs

@@ -6,10 +6,31 @@ using Microsoft.AspNetCore.Http;
 
 namespace MediaBrowser.Controller.Net
 {
+    /// <summary>
+    /// IAuthService.
+    /// </summary>
     public interface IAuthService
     {
-        void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
+        /// <summary>
+        /// Authenticate and authorize request.
+        /// </summary>
+        /// <param name="request">Request.</param>
+        /// <param name="authAttribtutes">Authorization attributes.</param>
+        void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
 
-        User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues);
+        /// <summary>
+        /// Authenticate and authorize request.
+        /// </summary>
+        /// <param name="request">Request.</param>
+        /// <param name="authAttribtutes">Authorization attributes.</param>
+        /// <returns>Authenticated user.</returns>
+        User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
+
+        /// <summary>
+        /// Authenticate request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Authorization information. Null if unauthenticated.</returns>
+        AuthorizationInfo Authenticate(HttpRequest request);
     }
 }

+ 11 - 0
MediaBrowser.Controller/Net/IAuthorizationContext.cs

@@ -1,7 +1,11 @@
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 
 namespace MediaBrowser.Controller.Net
 {
+    /// <summary>
+    /// IAuthorization context.
+    /// </summary>
     public interface IAuthorizationContext
     {
         /// <summary>
@@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
         /// <param name="requestContext">The request context.</param>
         /// <returns>AuthorizationInfo.</returns>
         AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
+
+        /// <summary>
+        /// Gets the authorization information.
+        /// </summary>
+        /// <param name="requestContext">The request context.</param>
+        /// <returns>AuthorizationInfo.</returns>
+        AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
     }
 }

+ 21 - 50
tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Linq;
 using System.Security.Claims;
-using System.Text.Encodings.Web;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
@@ -9,7 +8,6 @@ using Jellyfin.Api.Auth;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Http;
@@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
         private readonly IFixture _fixture;
 
         private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
-        private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
-        private readonly Mock<ISystemClock> _clockMock;
-        private readonly Mock<IServiceProvider> _serviceProviderMock;
-        private readonly Mock<IAuthenticationService> _authenticationServiceMock;
-        private readonly UrlEncoder _urlEncoder;
-        private readonly HttpContext _context;
 
         private readonly CustomAuthenticationHandler _sut;
         private readonly AuthenticationScheme _scheme;
@@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
             AllowFixtureCircularDependencies();
 
             _jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
-            _optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
-            _clockMock = _fixture.Freeze<Mock<ISystemClock>>();
-            _serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
-            _authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
+            var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
+            var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
+            var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
             _fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
 
-            _urlEncoder = UrlEncoder.Default;
+            serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
+                .Returns(authenticationServiceMock.Object);
 
-            _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
-                .Returns(_authenticationServiceMock.Object);
-
-            _optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
+            optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
                 .Returns(new AuthenticationSchemeOptions
                 {
                     ForwardAuthenticate = null
                 });
 
-            _context = new DefaultHttpContext
+            HttpContext context = new DefaultHttpContext
             {
-                RequestServices = _serviceProviderMock.Object
+                RequestServices = serviceProviderMock.Object
             };
 
             _scheme = new AuthenticationScheme(
@@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
                 typeof(CustomAuthenticationHandler));
 
             _sut = _fixture.Create<CustomAuthenticationHandler>();
-            _sut.InitializeAsync(_scheme, _context).Wait();
-        }
-
-        [Fact]
-        public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
-        {
-            _jellyfinAuthServiceMock.Setup(
-                    a => a.Authenticate(
-                        It.IsAny<HttpRequest>(),
-                        It.IsAny<AuthenticatedAttribute>()))
-                .Returns((User?)null);
-
-            var authenticateResult = await _sut.AuthenticateAsync();
-
-            Assert.False(authenticateResult.Succeeded);
-            Assert.True(authenticateResult.None);
-            // TODO return when legacy API is removed.
-            // Assert.Equal("Invalid user", authenticateResult.Failure.Message);
+            _sut.InitializeAsync(_scheme, context).Wait();
         }
 
         [Fact]
@@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
 
             _jellyfinAuthServiceMock.Setup(
                     a => a.Authenticate(
-                        It.IsAny<HttpRequest>(),
-                        It.IsAny<AuthenticatedAttribute>()))
+                        It.IsAny<HttpRequest>()))
                 .Throws(new SecurityException(errorMessage));
 
             var authenticateResult = await _sut.AuthenticateAsync();
@@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
         [Fact]
         public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
         {
-            var user = SetupUser();
+            var authorizationInfo = SetupUser();
             var authenticateResult = await _sut.AuthenticateAsync();
 
-            Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username));
+            Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
         }
 
         [Theory]
@@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
         [InlineData(false)]
         public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
         {
-            var user = SetupUser(isAdmin);
+            var authorizationInfo = SetupUser(isAdmin);
             var authenticateResult = await _sut.AuthenticateAsync();
 
-            var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
+            var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
             Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
         }
 
@@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
             Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
         }
 
-        private User SetupUser(bool isAdmin = false)
+        private AuthorizationInfo SetupUser(bool isAdmin = false)
         {
-            var user = _fixture.Create<User>();
-            user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
+            var authorizationInfo = _fixture.Create<AuthorizationInfo>();
+            authorizationInfo.User = _fixture.Create<User>();
+            authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
 
             _jellyfinAuthServiceMock.Setup(
                     a => a.Authenticate(
-                        It.IsAny<HttpRequest>(),
-                        It.IsAny<AuthenticatedAttribute>()))
-                .Returns(user);
+                        It.IsAny<HttpRequest>()))
+                .Returns(authorizationInfo);
 
-            return user;
+            return authorizationInfo;
         }
 
         private void AllowFixtureCircularDependencies()

+ 53 - 0
tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs

@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
+{
+    public class DefaultAuthorizationHandlerTests
+    {
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
+        private readonly DefaultAuthorizationHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+
+        public DefaultAuthorizationHandlerTests()
+        {
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+            _sut = fixture.Create<DefaultAuthorizationHandler>();
+        }
+
+        [Theory]
+        [InlineData(UserRoles.Administrator)]
+        [InlineData(UserRoles.Guest)]
+        [InlineData(UserRoles.User)]
+        public async Task ShouldSucceedOnUser(string userRole)
+        {
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var claims = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                userRole);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
+
+            await _sut.HandleAsync(context);
+            Assert.True(context.HasSucceeded);
+        }
+    }
+}

+ 20 - 26
tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs

@@ -1,13 +1,13 @@
 using System.Collections.Generic;
-using System.Security.Claims;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
+using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Moq;
 using Xunit;
 
@@ -18,12 +18,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         private readonly Mock<IConfigurationManager> _configurationManagerMock;
         private readonly List<IAuthorizationRequirement> _requirements;
         private readonly FirstTimeSetupOrElevatedHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
 
         public FirstTimeSetupOrElevatedHandlerTests()
         {
             var fixture = new Fixture().Customize(new AutoMoqCustomization());
             _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
             _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
 
             _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
         }
@@ -34,9 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         [InlineData(UserRoles.User)]
         public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
         {
-            SetupConfigurationManager(false);
-            var user = SetupUser(userRole);
-            var context = new AuthorizationHandlerContext(_requirements, user, null);
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
+            var claims = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                userRole);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.True(context.HasSucceeded);
@@ -48,30 +56,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         [InlineData(UserRoles.User, false)]
         public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
         {
-            SetupConfigurationManager(true);
-            var user = SetupUser(userRole);
-            var context = new AuthorizationHandlerContext(_requirements, user, null);
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var claims = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                userRole);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.Equal(shouldSucceed, context.HasSucceeded);
         }
-
-        private static ClaimsPrincipal SetupUser(string role)
-        {
-            var claims = new[] { new Claim(ClaimTypes.Role, role) };
-            var identity = new ClaimsIdentity(claims);
-            return new ClaimsPrincipal(identity);
-        }
-
-        private void SetupConfigurationManager(bool startupWizardCompleted)
-        {
-            var commonConfiguration = new BaseApplicationConfiguration
-            {
-                IsStartupWizardCompleted = startupWizardCompleted
-            };
-
-            _configurationManagerMock.Setup(c => c.CommonConfiguration)
-                .Returns(commonConfiguration);
-        }
     }
 }

+ 62 - 0
tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
+{
+    public class IgnoreScheduleHandlerTests
+    {
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
+        private readonly IgnoreScheduleHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+
+        /// <summary>
+        /// Globally disallow access.
+        /// </summary>
+        private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
+
+        public IgnoreScheduleHandlerTests()
+        {
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new IgnoreScheduleRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+            _sut = fixture.Create<IgnoreScheduleHandler>();
+        }
+
+        [Theory]
+        [InlineData(UserRoles.Administrator, true)]
+        [InlineData(UserRoles.User, true)]
+        [InlineData(UserRoles.Guest, true)]
+        public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed)
+        {
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var claims = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                role,
+                _accessSchedules);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
+
+            await _sut.HandleAsync(context);
+            Assert.Equal(shouldSucceed, context.HasSucceeded);
+        }
+    }
+}

+ 58 - 0
tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs

@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
+{
+    public class LocalAccessHandlerTests
+    {
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
+        private readonly LocalAccessHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+        private readonly Mock<INetworkManager> _networkManagerMock;
+
+        public LocalAccessHandlerTests()
+        {
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new LocalAccessRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+            _networkManagerMock = fixture.Freeze<Mock<INetworkManager>>();
+
+            _sut = fixture.Create<LocalAccessHandler>();
+        }
+
+        [Theory]
+        [InlineData(true, true)]
+        [InlineData(false, false)]
+        public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
+        {
+            _networkManagerMock
+                .Setup(n => n.IsInLocalNetwork(It.IsAny<string>()))
+                .Returns(isInLocalNetwork);
+
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var claims = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                UserRoles.User);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
+            await _sut.HandleAsync(context);
+            Assert.Equal(shouldSucceed, context.HasSucceeded);
+        }
+    }
+}

+ 23 - 8
tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs

@@ -1,20 +1,35 @@
 using System.Collections.Generic;
-using System.Security.Claims;
 using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
 using Xunit;
 
 namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
 {
     public class RequiresElevationHandlerTests
     {
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
         private readonly RequiresElevationHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
 
         public RequiresElevationHandlerTests()
         {
-            _sut = new RequiresElevationHandler();
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+            _sut = fixture.Create<RequiresElevationHandler>();
         }
 
         [Theory]
@@ -23,13 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
         [InlineData(UserRoles.Guest, false)]
         public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
         {
-            var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
-
-            var claims = new[] { new Claim(ClaimTypes.Role, role) };
-            var identity = new ClaimsIdentity(claims);
-            var user = new ClaimsPrincipal(identity);
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var claims = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                role);
 
-            var context = new AuthorizationHandlerContext(requirements, user, null);
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.Equal(shouldSucceed, context.HasSucceeded);

+ 1 - 0
tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj

@@ -35,6 +35,7 @@
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
     <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
+    <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
   </ItemGroup>
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

+ 81 - 0
tests/Jellyfin.Api.Tests/TestHelpers.cs

@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
+
+namespace Jellyfin.Api.Tests
+{
+    public static class TestHelpers
+    {
+        public static ClaimsPrincipal SetupUser(
+            Mock<IUserManager> userManagerMock,
+            Mock<IHttpContextAccessor> httpContextAccessorMock,
+            string role,
+            IEnumerable<AccessSchedule>? accessSchedules = null)
+        {
+            var user = new User(
+                "jellyfin",
+                typeof(DefaultAuthenticationProvider).FullName,
+                typeof(DefaultPasswordResetProvider).FullName);
+
+            // Set administrator flag.
+            user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
+
+            // Add access schedules if set.
+            if (accessSchedules != null)
+            {
+                foreach (var accessSchedule in accessSchedules)
+                {
+                    user.AccessSchedules.Add(accessSchedule);
+                }
+            }
+
+            var claims = new[]
+            {
+                new Claim(ClaimTypes.Role, role),
+                new Claim(ClaimTypes.Name, "jellyfin"),
+                new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.Device, "test"),
+                new Claim(InternalClaimTypes.Client, "test"),
+                new Claim(InternalClaimTypes.Version, "test"),
+                new Claim(InternalClaimTypes.Token, "test"),
+            };
+
+            var identity = new ClaimsIdentity(claims);
+
+            userManagerMock
+                .Setup(u => u.GetUserById(It.IsAny<Guid>()))
+                .Returns(user);
+
+            httpContextAccessorMock
+                .Setup(h => h.HttpContext.Connection.RemoteIpAddress)
+                .Returns(new IPAddress(0));
+
+            return new ClaimsPrincipal(identity);
+        }
+
+        public static void SetupConfigurationManager(in Mock<IConfigurationManager> configurationManagerMock, bool startupWizardCompleted)
+        {
+            var commonConfiguration = new BaseApplicationConfiguration
+            {
+                IsStartupWizardCompleted = startupWizardCompleted
+            };
+
+            configurationManagerMock
+                .Setup(c => c.CommonConfiguration)
+                .Returns(commonConfiguration);
+        }
+    }
+}