Bladeren bron

Fix issues with QuickConnect and AuthenticationDb

crobibero 3 jaren geleden
bovenliggende
commit
397868be95

+ 74 - 11
Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.QuickConnect;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.QuickConnect;
@@ -29,8 +30,9 @@ namespace Emby.Server.Implementations.QuickConnect
         /// </summary>
         private const int Timeout = 10;
 
-        private readonly RNGCryptoServiceProvider _rng = new();
-        private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new();
+        private readonly RNGCryptoServiceProvider _rng = new ();
+        private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
+        private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
 
         private readonly IServerConfigurationManager _config;
         private readonly ILogger<QuickConnectManager> _logger;
@@ -68,14 +70,41 @@ namespace Emby.Server.Implementations.QuickConnect
         }
 
         /// <inheritdoc/>
-        public QuickConnectResult TryConnect()
+        public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo)
         {
+            if (string.IsNullOrEmpty(authorizationInfo.DeviceId))
+            {
+                throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required");
+            }
+
+            if (string.IsNullOrEmpty(authorizationInfo.Device))
+            {
+                throw new ArgumentException(nameof(authorizationInfo.Device) + " is required");
+            }
+
+            if (string.IsNullOrEmpty(authorizationInfo.Client))
+            {
+                throw new ArgumentException(nameof(authorizationInfo.Client) + " is required");
+            }
+
+            if (string.IsNullOrEmpty(authorizationInfo.Version))
+            {
+                throw new ArgumentException(nameof(authorizationInfo.Version) + "is required");
+            }
+
             AssertActive();
             ExpireRequests();
 
             var secret = GenerateSecureRandom();
             var code = GenerateCode();
-            var result = new QuickConnectResult(secret, code, DateTime.UtcNow);
+            var result = new QuickConnectResult(
+                secret,
+                code,
+                DateTime.UtcNow,
+                authorizationInfo.DeviceId,
+                authorizationInfo.Device,
+                authorizationInfo.Client,
+                authorizationInfo.Version);
 
             _currentRequests[code] = result;
             return result;
@@ -135,19 +164,41 @@ namespace Emby.Server.Implementations.QuickConnect
                 throw new InvalidOperationException("Request is already authorized");
             }
 
-            var token = Guid.NewGuid();
-            result.Authentication = token;
-
             // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
-            result.DateAdded = DateTime.Now.Add(TimeSpan.FromMinutes(1));
+            result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1));
 
-            await _sessionManager.AuthenticateQuickConnect(userId).ConfigureAwait(false);
+            var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
+            {
+                UserId = userId,
+                DeviceId = result.DeviceId,
+                DeviceName = result.DeviceName,
+                App = result.AppName,
+                AppVersion = result.AppVersion
+            }).ConfigureAwait(false);
+
+            _authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult);
+            result.Authenticated = true;
+            _currentRequests[code] = result;
 
-            _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
+            _logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId);
 
             return true;
         }
 
+        /// <inheritdoc/>
+        public AuthenticationResult GetAuthorizedRequest(string secret)
+        {
+            AssertActive();
+            ExpireRequests();
+
+            if (!_authorizedSecrets.TryGetValue(secret, out var result))
+            {
+                throw new ResourceNotFoundException("Unable to find request");
+            }
+
+            return result.AuthenticationResult;
+        }
+
         /// <summary>
         /// Dispose.
         /// </summary>
@@ -189,7 +240,7 @@ namespace Emby.Server.Implementations.QuickConnect
             // Expire stale connection requests
             foreach (var (_, currentRequest) in _currentRequests)
             {
-                if (expireAll || currentRequest.DateAdded > minTime)
+                if (expireAll || currentRequest.DateAdded < minTime)
                 {
                     var code = currentRequest.Code;
                     _logger.LogDebug("Removing expired request {Code}", code);
@@ -200,6 +251,18 @@ namespace Emby.Server.Implementations.QuickConnect
                     }
                 }
             }
+
+            foreach (var (secret, (timestamp, _)) in _authorizedSecrets)
+            {
+                if (expireAll || timestamp < minTime)
+                {
+                    _logger.LogDebug("Removing expired secret {Secret}", secret);
+                    if (!_authorizedSecrets.TryRemove(secret, out _))
+                    {
+                        _logger.LogWarning("Secret {Secret} already expired", secret);
+                    }
+                }
+            }
         }
     }
 }

+ 9 - 4
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1432,16 +1432,21 @@ namespace Emby.Server.Implementations.Session
         /// <summary>
         /// Authenticates the new session.
         /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{SessionInfo}.</returns>
+        /// <param name="request">The authenticationrequest.</param>
+        /// <returns>The authentication result.</returns>
         public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
         {
             return AuthenticateNewSessionInternal(request, true);
         }
 
-        public Task<AuthenticationResult> AuthenticateQuickConnect(Guid userId)
+        /// <summary>
+        /// Directly authenticates the session without enforcing password.
+        /// </summary>
+        /// <param name="request">The authentication request.</param>
+        /// <returns>The authentication result.</returns>
+        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
         {
-            return AuthenticateNewSessionInternal(new AuthenticationRequest { UserId = userId }, false);
+            return AuthenticateNewSessionInternal(request, false);
         }
 
         private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)

+ 8 - 3
Jellyfin.Api/Controllers/QuickConnectController.cs

@@ -4,6 +4,7 @@ using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.QuickConnect;
 using MediaBrowser.Model.QuickConnect;
 using Microsoft.AspNetCore.Authorization;
@@ -18,14 +19,17 @@ namespace Jellyfin.Api.Controllers
     public class QuickConnectController : BaseJellyfinApiController
     {
         private readonly IQuickConnect _quickConnect;
+        private readonly IAuthorizationContext _authContext;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
         /// </summary>
         /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
-        public QuickConnectController(IQuickConnect quickConnect)
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
         {
             _quickConnect = quickConnect;
+            _authContext = authContext;
         }
 
         /// <summary>
@@ -48,11 +52,12 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
         [HttpGet("Initiate")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QuickConnectResult> Initiate()
+        public async Task<ActionResult<QuickConnectResult>> Initiate()
         {
             try
             {
-                return _quickConnect.TryConnect();
+                var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+                return _quickConnect.TryConnect(auth);
             }
             catch (AuthenticationException)
             {

+ 8 - 15
Jellyfin.Api/Controllers/UserController.cs

@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.QuickConnect;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
@@ -38,6 +39,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IAuthorizationContext _authContext;
         private readonly IServerConfigurationManager _config;
         private readonly ILogger _logger;
+        private readonly IQuickConnect _quickConnectManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UserController"/> class.
@@ -49,6 +51,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+        /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
         public UserController(
             IUserManager userManager,
             ISessionManager sessionManager,
@@ -56,7 +59,8 @@ namespace Jellyfin.Api.Controllers
             IDeviceManager deviceManager,
             IAuthorizationContext authContext,
             IServerConfigurationManager config,
-            ILogger<UserController> logger)
+            ILogger<UserController> logger,
+            IQuickConnect quickConnectManager)
         {
             _userManager = userManager;
             _sessionManager = sessionManager;
@@ -65,6 +69,7 @@ namespace Jellyfin.Api.Controllers
             _authContext = authContext;
             _config = config;
             _logger = logger;
+            _quickConnectManager = quickConnectManager;
         }
 
         /// <summary>
@@ -228,23 +233,11 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
         [HttpPost("AuthenticateWithQuickConnect")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+        public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
         {
-            var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
-
             try
             {
-                var authRequest = new AuthenticationRequest
-                {
-                    App = auth.Client,
-                    AppVersion = auth.Version,
-                    DeviceId = auth.DeviceId,
-                    DeviceName = auth.Device,
-                };
-
-                return await _sessionManager.AuthenticateQuickConnect(
-                    authRequest,
-                    request.Token).ConfigureAwait(false);
+                return _quickConnectManager.GetAuthorizedRequest(request.Secret);
             }
             catch (SecurityException e)
             {

+ 2 - 2
Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs

@@ -8,9 +8,9 @@ namespace Jellyfin.Api.Models.UserDtos
     public class QuickConnectDto
     {
         /// <summary>
-        /// Gets or sets the quick connect token.
+        /// Gets or sets the quick connect secret.
         /// </summary>
         [Required]
-        public string? Token { get; set; }
+        public string Secret { get; set; } = null!;
     }
 }

+ 11 - 2
MediaBrowser.Controller/QuickConnect/IQuickConnect.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Threading.Tasks;
-using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.QuickConnect;
 
 namespace MediaBrowser.Controller.QuickConnect
@@ -18,8 +19,9 @@ namespace MediaBrowser.Controller.QuickConnect
         /// <summary>
         /// Initiates a new quick connect request.
         /// </summary>
+        /// <param name="authorizationInfo">The initiator authorization info.</param>
         /// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
-        QuickConnectResult TryConnect();
+        QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo);
 
         /// <summary>
         /// Checks the status of an individual request.
@@ -35,5 +37,12 @@ namespace MediaBrowser.Controller.QuickConnect
         /// <param name="code">Identifying code for the request.</param>
         /// <returns>A boolean indicating if the authorization completed successfully.</returns>
         Task<bool> AuthorizeRequest(Guid userId, string code);
+
+        /// <summary>
+        /// Gets the authorized request for the secret.
+        /// </summary>
+        /// <param name="secret">The secret.</param>
+        /// <returns>The authentication result.</returns>
+        AuthenticationResult GetAuthorizedRequest(string secret);
     }
 }

+ 1 - 6
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -273,12 +273,7 @@ namespace MediaBrowser.Controller.Session
         /// <returns>Task{SessionInfo}.</returns>
         Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
 
-        /// <summary>
-        /// Authenticates a new session with quick connect.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Task{SessionInfo}.</returns>
-        Task<AuthenticationResult> AuthenticateQuickConnect(Guid userId);
+        Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request);
 
         /// <summary>
         /// Reports the capabilities.

+ 35 - 5
MediaBrowser.Model/QuickConnect/QuickConnectResult.cs

@@ -13,17 +13,32 @@ namespace MediaBrowser.Model.QuickConnect
         /// <param name="secret">The secret used to query the request state.</param>
         /// <param name="code">The code used to allow the request.</param>
         /// <param name="dateAdded">The time when the request was created.</param>
-        public QuickConnectResult(string secret, string code, DateTime dateAdded)
+        /// <param name="deviceId">The requesting device id.</param>
+        /// <param name="deviceName">The requesting device name.</param>
+        /// <param name="appName">The requesting app name.</param>
+        /// <param name="appVersion">The requesting app version.</param>
+        public QuickConnectResult(
+            string secret,
+            string code,
+            DateTime dateAdded,
+            string deviceId,
+            string deviceName,
+            string appName,
+            string appVersion)
         {
             Secret = secret;
             Code = code;
             DateAdded = dateAdded;
+            DeviceId = deviceId;
+            DeviceName = deviceName;
+            AppName = appName;
+            AppVersion = appVersion;
         }
 
         /// <summary>
-        /// Gets a value indicating whether this request is authorized.
+        /// Gets or sets a value indicating whether this request is authorized.
         /// </summary>
-        public bool Authenticated => Authentication != null;
+        public bool Authenticated { get; set; }
 
         /// <summary>
         /// Gets the secret value used to uniquely identify this request. Can be used to retrieve authentication information.
@@ -36,9 +51,24 @@ namespace MediaBrowser.Model.QuickConnect
         public string Code { get; }
 
         /// <summary>
-        /// Gets or sets the private access token.
+        /// Gets the requesting device id.
         /// </summary>
-        public Guid? Authentication { get; set; }
+        public string DeviceId { get; }
+
+        /// <summary>
+        /// Gets the requesting device name.
+        /// </summary>
+        public string DeviceName { get; }
+
+        /// <summary>
+        /// Gets the requesting app name.
+        /// </summary>
+        public string AppName { get; }
+
+        /// <summary>
+        /// Gets the requesting app version.
+        /// </summary>
+        public string AppVersion { get; }
 
         /// <summary>
         /// Gets or sets the DateTime that this request was created.