JWT Query Parameter Auth for IPTV Links (#1215)
* JWT Auth * Standardized url variable additions * formatting and minor refactoring * this isn't needed * allow channel logos without auth * update changelog --------- Co-authored-by: Ministorm3 <4474921+Ministorm3@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,7 @@ using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Iptv;
|
||||
using ErsatzTV.Filters;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -16,6 +17,7 @@ namespace ErsatzTV.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[ServiceFilter(typeof(ConditionalIptvAuthorizeFilter))]
|
||||
public class IptvController : ControllerBase
|
||||
{
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
@@ -36,12 +38,23 @@ public class IptvController : ControllerBase
|
||||
public Task<IActionResult> GetChannelPlaylist(
|
||||
[FromQuery]
|
||||
string mode = "mixed") =>
|
||||
_mediator.Send(new GetChannelPlaylist(Request.Scheme, Request.Host.ToString(), Request.PathBase, mode))
|
||||
_mediator.Send(
|
||||
new GetChannelPlaylist(
|
||||
Request.Scheme,
|
||||
Request.Host.ToString(),
|
||||
Request.PathBase,
|
||||
mode,
|
||||
Request.Query["access_token"]))
|
||||
.Map<ChannelPlaylist, IActionResult>(Ok);
|
||||
|
||||
[HttpGet("iptv/xmltv.xml")]
|
||||
public Task<IActionResult> GetGuide() =>
|
||||
_mediator.Send(new GetChannelGuide(Request.Scheme, Request.Host.ToString(), Request.PathBase))
|
||||
_mediator.Send(
|
||||
new GetChannelGuide(
|
||||
Request.Scheme,
|
||||
Request.Host.ToString(),
|
||||
Request.PathBase,
|
||||
Request.Query["access_token"]))
|
||||
.Map<ChannelGuide, IActionResult>(Ok);
|
||||
|
||||
[HttpGet("iptv/hdhr/channel/{channelNumber}.ts")]
|
||||
@@ -77,7 +90,7 @@ public class IptvController : ControllerBase
|
||||
mode = "ts";
|
||||
break;
|
||||
default:
|
||||
return Redirect($"~/iptv/channel/{channelNumber}.m3u8");
|
||||
return Redirect($"~/iptv/channel/{channelNumber}.m3u8{AccessTokenQuery()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +139,7 @@ public class IptvController : ControllerBase
|
||||
public async Task<IActionResult> GetLivePlaylist(string channelNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
// _logger.LogDebug("Checking for session worker for channel {Channel}", channelNumber);
|
||||
|
||||
|
||||
if (_ffmpegSegmenterService.SessionWorkers.TryGetValue(channelNumber, out IHlsSessionWorker worker))
|
||||
{
|
||||
// _logger.LogDebug("Trimming playlist for channel {Channel}", channelNumber);
|
||||
@@ -168,7 +181,7 @@ public class IptvController : ControllerBase
|
||||
mode = "segmenter";
|
||||
break;
|
||||
default:
|
||||
return Redirect($"~/iptv/channel/{channelNumber}.ts");
|
||||
return Redirect($"~/iptv/channel/{channelNumber}.ts{AccessTokenQuery()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,7 +209,7 @@ public class IptvController : ControllerBase
|
||||
"Session is already active; returning multi-variant playlist for channel {Channel}",
|
||||
channelNumber);
|
||||
return Content(GetMultiVariantPlaylist(channelNumber), "application/x-mpegurl");
|
||||
// return RedirectPreserveMethod($"iptv/session/{channelNumber}/hls.m3u8");
|
||||
// return RedirectPreserveMethod($"iptv/session/{channelNumber}/hls.m3u8");
|
||||
default:
|
||||
_logger.LogWarning(
|
||||
"Failed to start segmenter for channel {ChannelNumber}: {Error}",
|
||||
@@ -236,5 +249,9 @@ public class IptvController : ControllerBase
|
||||
$@"#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=10000000
|
||||
{Request.Scheme}://{Request.Host}/iptv/session/{channelNumber}/hls.m3u8";
|
||||
{Request.Scheme}://{Request.Host}/iptv/session/{channelNumber}/hls.m3u8{AccessTokenQuery()}";
|
||||
|
||||
private string AccessTokenQuery() => string.IsNullOrWhiteSpace(Request.Query["access_token"])
|
||||
? string.Empty
|
||||
: $"?access_token={Request.Query["access_token"]}";
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.4.2" />
|
||||
<PackageReference Include="Markdig" Version="0.31.0" />
|
||||
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace ErsatzTV.Filters;
|
||||
|
||||
public class ConditionalIptvAuthorizeFilter : AuthorizeFilter
|
||||
{
|
||||
public ConditionalIptvAuthorizeFilter(string policy) : base(
|
||||
new AuthorizationPolicyBuilder().RequireAuthenticatedUser().AddAuthenticationSchemes("jwt")
|
||||
.RequireAssertion(_ => JwtHelper.IsEnabled).Build())
|
||||
{
|
||||
}
|
||||
|
||||
public override Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
// allow logos through without authorization, since they're also used in the management ui
|
||||
if (JwtHelper.IsEnabled && !context.HttpContext.Request.Path.StartsWithSegments("/iptv/logos"))
|
||||
{
|
||||
return base.OnAuthorizationAsync(context);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
namespace ErsatzTV;
|
||||
|
||||
public static class JwtHelper
|
||||
{
|
||||
public static void Init(IConfiguration configuration)
|
||||
{
|
||||
string issuerSigningKey = configuration["JWT:IssuerSigningKey"];
|
||||
IsEnabled = !string.IsNullOrWhiteSpace(issuerSigningKey);
|
||||
if (IsEnabled)
|
||||
{
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(issuerSigningKey!));
|
||||
}
|
||||
}
|
||||
|
||||
public static SymmetricSecurityKey IssuerSigningKey { get; private set; }
|
||||
public static bool IsEnabled { get; private set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inherits LayoutComponentBase
|
||||
@using System.Reflection
|
||||
@using ErsatzTV.Extensions
|
||||
@using ErsatzTV.Application.Search
|
||||
|
||||
+69
-1
@@ -34,6 +34,7 @@ using ErsatzTV.Core.Trakt;
|
||||
using ErsatzTV.FFmpeg.Capabilities;
|
||||
using ErsatzTV.FFmpeg.Pipeline;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using ErsatzTV.Filters;
|
||||
using ErsatzTV.Formatters;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Data.Repositories;
|
||||
@@ -60,12 +61,16 @@ using FluentValidation.AspNetCore;
|
||||
using Ganss.Xss;
|
||||
using MediatR.Courier.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.IO;
|
||||
using MudBlazor.Services;
|
||||
using Newtonsoft.Json;
|
||||
@@ -121,6 +126,7 @@ public class Startup
|
||||
});
|
||||
|
||||
OidcHelper.Init(Configuration);
|
||||
JwtHelper.Init(Configuration);
|
||||
|
||||
if (OidcHelper.IsEnabled)
|
||||
{
|
||||
@@ -178,6 +184,66 @@ public class Startup
|
||||
});
|
||||
}
|
||||
|
||||
if (JwtHelper.IsEnabled)
|
||||
{
|
||||
services.AddAuthentication().AddJwtBearer(
|
||||
"jwt",
|
||||
options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = JwtHelper.IssuerSigningKey,
|
||||
ValidateLifetime = true
|
||||
};
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = static context =>
|
||||
{
|
||||
StringValues token = context.Request.Query["access_token"];
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
context.Token = token;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (OidcHelper.IsEnabled || JwtHelper.IsEnabled)
|
||||
{
|
||||
services.AddAuthorization(
|
||||
options =>
|
||||
{
|
||||
if (OidcHelper.IsEnabled)
|
||||
{
|
||||
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
|
||||
"cookie",
|
||||
"oidc");
|
||||
|
||||
defaultAuthorizationPolicyBuilder =
|
||||
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
|
||||
|
||||
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
|
||||
}
|
||||
|
||||
if (JwtHelper.IsEnabled)
|
||||
{
|
||||
var onlyJwtSchemePolicyBuilder = new AuthorizationPolicyBuilder("jwt");
|
||||
options.AddPolicy(
|
||||
"JwtOnlyScheme",
|
||||
onlyJwtSchemePolicyBuilder
|
||||
.RequireAuthenticatedUser()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
services.AddCors(
|
||||
o => o.AddPolicy(
|
||||
"AllowAll",
|
||||
@@ -206,6 +272,8 @@ public class Startup
|
||||
opt.SerializerSettings.Converters.Add(new StringEnumConverter());
|
||||
});
|
||||
|
||||
services.AddScoped(_ => new ConditionalIptvAuthorizeFilter("JwtOnlyScheme"));
|
||||
|
||||
services.AddFluentValidationAutoValidation();
|
||||
services.AddValidatorsFromAssemblyContaining<Startup>();
|
||||
|
||||
@@ -386,7 +454,7 @@ public class Startup
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
|
||||
if (OidcHelper.IsEnabled)
|
||||
{
|
||||
app.UseAuthentication();
|
||||
|
||||
Reference in New Issue
Block a user