Merge pull request #4330 from crobibero/api-key-auth

Fix ApiKey authentication
pull/3683/head
Claus Vium 4 years ago committed by GitHub
commit c17f84ae48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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);
AccessToken = token }
});
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; var result = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
if (originalAuthenticationInfo != null) var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace if (originalAuthenticationInfo != null)
if (string.IsNullOrWhiteSpace(authInfo.Client)) {
{ var updateToken = false;
authInfo.Client = originalAuthenticationInfo.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) // TODO: Remove these checks for IsNullOrWhiteSpace
{ if (string.IsNullOrWhiteSpace(authInfo.Client))
authInfo.DeviceId = originalAuthenticationInfo.DeviceId; {
} authInfo.Client = originalAuthenticationInfo.AppName;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; {
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
if (string.IsNullOrWhiteSpace(authInfo.Device)) // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
{ var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
authInfo.Device = originalAuthenticationInfo.DeviceName;
}
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version)) if (string.IsNullOrWhiteSpace(authInfo.Device))
{ {
authInfo.Version = originalAuthenticationInfo.AppVersion; authInfo.Device = originalAuthenticationInfo.DeviceName;
} }
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{ {
if (allowTokenInfoUpdate) updateToken = true;
{ originalAuthenticationInfo.DeviceName = authInfo.Device;
updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
}
} }
}
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3) if (string.IsNullOrWhiteSpace(authInfo.Version))
{
authInfo.Version = originalAuthenticationInfo.AppVersion;
}
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{ {
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true; updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
} }
}
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{ {
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{ {
originalAuthenticationInfo.UserName = authInfo.User.Username; authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
updateToken = true;
}
}
if (updateToken) if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{ {
_authRepo.Update(originalAuthenticationInfo); originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
} }
authInfo.IsApiKey = true;
}
else
{
authInfo.IsApiKey = false;
}
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…
Cancel
Save