aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs85
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs13
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs11
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs23
-rw-r--r--Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs4
-rw-r--r--MediaBrowser.Controller/QuickConnect/IQuickConnect.cs13
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs7
-rw-r--r--MediaBrowser.Model/QuickConnect/QuickConnectResult.cs40
8 files changed, 148 insertions, 48 deletions
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index afc08fc26..ae773c658 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/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);
+ }
+ }
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 29b545583..40a346e95 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/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)
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 56fef08a9..87b78fe93 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/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)
{
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 8e2298bb7..4263d4fe5 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/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)
{
diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
index c3a2d5cec..9493c08c2 100644
--- a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
+++ b/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!;
}
}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index 616409533..ec3706773 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/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);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 8be9ff521..88a905166 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/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.
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
index d180d2986..35a82f47c 100644
--- a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
+++ b/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.