Fix issues with QuickConnect and AuthenticationDb

This commit is contained in:
crobibero 2021-06-23 21:07:08 -06:00
parent ae878fa051
commit 397868be95
8 changed files with 148 additions and 48 deletions

View file

@ -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);
}
}
}
}
}
}

View file

@ -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)

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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!;
}
}

View file

@ -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);
}
}

View file

@ -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.

View file

@ -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.