Fix issues with QuickConnect and AuthenticationDb
This commit is contained in:
parent
ae878fa051
commit
397868be95
|
@ -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);
|
||||
|
||||
_logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
|
||||
_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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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!;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user