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:
Jason Dove
2023-03-17 09:33:15 -05:00
committed by GitHub
co-authored by Ministorm3
parent fdab54a055
commit 9ba0b844a1
13 changed files with 188 additions and 31 deletions
+24 -7
View File
@@ -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"]}";
}
+1
View File
@@ -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;
}
}
+20
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
@inherits LayoutComponentBase
@inherits LayoutComponentBase
@using System.Reflection
@using ErsatzTV.Extensions
@using ErsatzTV.Application.Search
+69 -1
View File
@@ -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();