// // Copyright (c) PlaceholderCompany. All rights reserved. // namespace Jellyfin.Api.Middleware; using System; using System.Globalization; using System.Security.Claims; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; /// /// WebSocket Authentication Middleware. /// Extracts and validates API tokens from WebSocket query strings. /// public class WebSocketAuthenticationMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The next delegate in the pipeline. /// The logger. public WebSocketAuthenticationMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } /// /// Executes the middleware action. /// /// The current HTTP context. /// The authentication service. /// The async task. public async Task Invoke(HttpContext httpContext, IAuthService authService) { // Only process WebSocket upgrade requests on the /socket endpoint if (httpContext.WebSockets.IsWebSocketRequestUpgrade && httpContext.Request.Path.StartsWithSegments("/socket", StringComparison.OrdinalIgnoreCase)) { await AuthenticateWebSocketRequest(httpContext, authService).ConfigureAwait(false); } await _next(httpContext).ConfigureAwait(false); } /// /// Authenticates WebSocket requests by extracting api_key from query string. /// /// The HTTP context. /// The authentication service. /// The async task. private async Task AuthenticateWebSocketRequest(HttpContext httpContext, IAuthService authService) { try { // Extract api_key from query string if (!httpContext.Request.Query.TryGetValue("api_key", out var apiKeyValues) || apiKeyValues.Count == 0) { _logger.LogDebug("WebSocket connection attempted without api_key query parameter. IP: {IP}", httpContext.Connection.RemoteIpAddress); return; } var apiKey = apiKeyValues[0]; if (string.IsNullOrWhiteSpace(apiKey)) { _logger.LogDebug("WebSocket connection attempted with empty api_key. IP: {IP}", httpContext.Connection.RemoteIpAddress); return; } // Authenticate using the provided api_key var authorizationInfo = await authService.Authenticate(httpContext.Request).ConfigureAwait(false); if (!authorizationInfo.HasToken) { _logger.LogDebug("WebSocket authentication failed: Invalid or expired token. IP: {IP}", httpContext.Connection.RemoteIpAddress); return; } // Set the authenticated user in the context for downstream middleware var role = UserRoles.User; if (authorizationInfo.IsApiKey || (authorizationInfo.User?.HasPermission(PermissionKind.IsAdministrator) ?? false)) { role = UserRoles.Administrator; } var claims = new[] { new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty), new Claim(ClaimTypes.Role, role), new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)), new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId ?? string.Empty), new Claim(InternalClaimTypes.Device, authorizationInfo.Device ?? string.Empty), new Claim(InternalClaimTypes.Client, authorizationInfo.Client ?? string.Empty), new Claim(InternalClaimTypes.Version, authorizationInfo.Version ?? string.Empty), new Claim(InternalClaimTypes.Token, authorizationInfo.Token), new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture)) }; var identity = new ClaimsIdentity(claims, "WebSocket"); var principal = new ClaimsPrincipal(identity); httpContext.User = principal; _logger.LogDebug("WebSocket authentication successful for user {Username}. IP: {IP}", authorizationInfo.User?.Username ?? "Unknown", httpContext.Connection.RemoteIpAddress); } catch (Exception ex) { _logger.LogDebug(ex, "Error during WebSocket authentication. IP: {IP}", httpContext.Connection.RemoteIpAddress); } } } /// /// Adds WebSocket authentication to the application pipeline. /// /// The application builder. /// The updated application builder. public static IApplicationBuilder UseWebSocketAuthentication(this IApplicationBuilder appBuilder) { return appBuilder.UseMiddleware(); }