mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Merge pull request #4330 from crobibero/api-key-auth
Fix ApiKey authentication
This commit is contained in:
commit
c17f84ae48
@ -19,12 +19,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
public AuthorizationInfo Authenticate(HttpRequest request)
|
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||||
{
|
{
|
||||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||||
if (auth?.User == null)
|
if (auth == null)
|
||||||
{
|
{
|
||||||
return null;
|
throw new SecurityException("Unauthenticated request.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.User.HasPermission(PermissionKind.IsDisabled))
|
if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
|
||||||
{
|
{
|
||||||
throw new SecurityException("User account has been disabled.");
|
throw new SecurityException("User account has been disabled.");
|
||||||
}
|
}
|
||||||
|
@ -111,81 +111,89 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
Token = token
|
Token = token
|
||||||
};
|
};
|
||||||
|
|
||||||
AuthenticationInfo originalAuthenticationInfo = null;
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
if (!string.IsNullOrWhiteSpace(token))
|
|
||||||
{
|
{
|
||||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
// Request doesn't contain a token.
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||||
|
{
|
||||||
|
AccessToken = token
|
||||||
|
});
|
||||||
|
|
||||||
|
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||||
|
|
||||||
|
if (originalAuthenticationInfo != null)
|
||||||
|
{
|
||||||
|
var updateToken = false;
|
||||||
|
|
||||||
|
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||||
|
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||||
{
|
{
|
||||||
AccessToken = token
|
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||||
});
|
}
|
||||||
|
|
||||||
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||||
|
|
||||||
if (originalAuthenticationInfo != null)
|
|
||||||
{
|
{
|
||||||
var updateToken = false;
|
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||||
{
|
|
||||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||||
|
{
|
||||||
|
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||||
|
}
|
||||||
|
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (allowTokenInfoUpdate)
|
||||||
{
|
{
|
||||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
updateToken = true;
|
||||||
|
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
{
|
||||||
|
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||||
|
}
|
||||||
|
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (allowTokenInfoUpdate)
|
||||||
|
{
|
||||||
|
updateToken = true;
|
||||||
|
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||||
{
|
{
|
||||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||||
}
|
updateToken = true;
|
||||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
}
|
||||||
{
|
|
||||||
if (allowTokenInfoUpdate)
|
|
||||||
{
|
|
||||||
updateToken = true;
|
|
||||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||||
{
|
{
|
||||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||||
}
|
|
||||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (allowTokenInfoUpdate)
|
|
||||||
{
|
|
||||||
updateToken = true;
|
|
||||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
authInfo.IsApiKey = true;
|
||||||
{
|
}
|
||||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
else
|
||||||
|
{
|
||||||
|
authInfo.IsApiKey = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
if (updateToken)
|
||||||
{
|
{
|
||||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
_authRepo.Update(originalAuthenticationInfo);
|
||||||
updateToken = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateToken)
|
|
||||||
{
|
|
||||||
_authRepo.Update(originalAuthenticationInfo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,13 @@ namespace Jellyfin.Api.Auth
|
|||||||
bool localAccessOnly = false,
|
bool localAccessOnly = false,
|
||||||
bool requiredDownloadPermission = false)
|
bool requiredDownloadPermission = false)
|
||||||
{
|
{
|
||||||
|
// ApiKey is currently global admin, always allow.
|
||||||
|
var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
|
||||||
|
if (isApiKey)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure claim has userId.
|
// Ensure claim has userId.
|
||||||
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
|
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
|
||||||
if (!userId.HasValue)
|
if (!userId.HasValue)
|
||||||
|
@ -43,24 +43,23 @@ namespace Jellyfin.Api.Auth
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var authorizationInfo = _authService.Authenticate(Request);
|
var authorizationInfo = _authService.Authenticate(Request);
|
||||||
if (authorizationInfo == null)
|
var role = UserRoles.User;
|
||||||
|
if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||||
{
|
{
|
||||||
return Task.FromResult(AuthenticateResult.NoResult());
|
role = UserRoles.Administrator;
|
||||||
// TODO return when legacy API is removed.
|
|
||||||
// Don't spam the log with "Invalid User"
|
|
||||||
// return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
|
new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
|
||||||
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
|
new Claim(ClaimTypes.Role, role),
|
||||||
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
|
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
|
||||||
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
|
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
|
||||||
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
|
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
|
||||||
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
|
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
|
||||||
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
|
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
|
||||||
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
|
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
|
||||||
|
new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
|
||||||
};
|
};
|
||||||
|
|
||||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||||
|
@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
|||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
||||||
{
|
{
|
||||||
var validated = ValidateClaims(context.User);
|
var validated = ValidateClaims(context.User);
|
||||||
if (!validated)
|
if (validated)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
context.Fail();
|
context.Fail();
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Succeed(requirement);
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
|
|||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
|
||||||
{
|
{
|
||||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
||||||
if (!validated)
|
if (validated)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
context.Fail();
|
context.Fail();
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Succeed(requirement);
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,13 +29,13 @@ namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
|||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
|
||||||
{
|
{
|
||||||
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
||||||
if (!validated)
|
if (validated)
|
||||||
{
|
{
|
||||||
context.Fail();
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -34,5 +34,10 @@
|
|||||||
/// Token.
|
/// Token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Token = "Jellyfin-Token";
|
public const string Token = "Jellyfin-Token";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is Api Key.
|
||||||
|
/// </summary>
|
||||||
|
public const string IsApiKey = "Jellyfin-IsApiKey";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,19 @@ namespace Jellyfin.Api.Helpers
|
|||||||
public static string? GetToken(in ClaimsPrincipal user)
|
public static string? GetToken(in ClaimsPrincipal user)
|
||||||
=> GetClaimValue(user, InternalClaimTypes.Token);
|
=> GetClaimValue(user, InternalClaimTypes.Token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a flag specifying whether the request is using an api key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">Current claims principal.</param>
|
||||||
|
/// <returns>The flag specifying whether the request is using an api key.</returns>
|
||||||
|
public static bool GetIsApiKey(in ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey);
|
||||||
|
return !string.IsNullOrEmpty(claimValue)
|
||||||
|
&& bool.TryParse(claimValue, out var parsedClaimValue)
|
||||||
|
&& parsedClaimValue;
|
||||||
|
}
|
||||||
|
|
||||||
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
|
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
|
||||||
{
|
{
|
||||||
return user?.Identities
|
return user?.Identities
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The request authorization info.
|
||||||
|
/// </summary>
|
||||||
public class AuthorizationInfo
|
public class AuthorizationInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -43,6 +44,14 @@ namespace MediaBrowser.Controller.Net
|
|||||||
/// <value>The token.</value>
|
/// <value>The token.</value>
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the authorization is from an api key.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsApiKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user making the request.
|
||||||
|
/// </summary>
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,6 +128,7 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
|
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
|
||||||
authorizationInfo.User = _fixture.Create<User>();
|
authorizationInfo.User = _fixture.Create<User>();
|
||||||
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
||||||
|
authorizationInfo.IsApiKey = false;
|
||||||
|
|
||||||
_jellyfinAuthServiceMock.Setup(
|
_jellyfinAuthServiceMock.Setup(
|
||||||
a => a.Authenticate(
|
a => a.Authenticate(
|
||||||
|
@ -45,7 +45,7 @@ namespace Jellyfin.Api.Tests
|
|||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Role, role),
|
new Claim(ClaimTypes.Role, role),
|
||||||
new Claim(ClaimTypes.Name, "jellyfin"),
|
new Claim(ClaimTypes.Name, "jellyfin"),
|
||||||
new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
|
new Claim(InternalClaimTypes.UserId, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)),
|
||||||
new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
|
new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
|
||||||
new Claim(InternalClaimTypes.Device, "test"),
|
new Claim(InternalClaimTypes.Device, "test"),
|
||||||
new Claim(InternalClaimTypes.Client, "test"),
|
new Claim(InternalClaimTypes.Client, "test"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user