Browse Source

add oidc support (#1133)

pull/1134/head
Jason Dove 3 years ago committed by GitHub
parent
commit
99b8038852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      CHANGELOG.md
  2. 24
      ErsatzTV/App.razor
  3. 13
      ErsatzTV/Controllers/AccountController.cs
  4. 2
      ErsatzTV/ErsatzTV.csproj
  5. 20
      ErsatzTV/OidcHelper.cs
  6. 103
      ErsatzTV/Shared/MainLayout.razor
  7. 67
      ErsatzTV/Startup.cs
  8. 2
      ErsatzTV/wwwroot/css/site.css

8
CHANGELOG.md

@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Attempt to release memory periodically
- Add SSO support via OIDC
- This only protects the management UI; all streaming endpoints will continue to allow anonymous access
- This can be configured with the following env vars (note the double underscore separator `__`)
- `OIDC__AUTHORITY`
- `OIDC__CLIENTID`
- `OIDC__CLIENTSECRET`
### Fixed
- Fix schedule editor crashing due to bad music video artist data
@ -18,6 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -18,6 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- This ensures errors will display even when hardware acceleration is misconfigured
- Call scanner process only when scanning is required based on library refresh interval
- Use lower process priority for scanner process with unforced (automatic) library scans
- Disable V2 UI by default
- V2 UI can be re-enabled by setting the env var `ETV_UI_V2` to any value
## [0.7.2-beta] - 2023-01-05
### Fixed

24
ErsatzTV/App.razor

@ -1,11 +1,13 @@ @@ -1,11 +1,13 @@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

13
ErsatzTV/Controllers/AccountController.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers;
[ApiController]
public class AccountController : ControllerBase
{
[HttpPost("account/logout")]
public IActionResult Logout()
{
return new SignOutResult(new[] { "oidc", "cookie" });
}
}

2
ErsatzTV/ErsatzTV.csproj

@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<Configurations>Debug;Release;Debug No Sync</Configurations>
<Platforms>AnyCPU</Platforms>
<UserSecretsId>bf31217d-f4ec-4520-8cc3-138059044ede</UserSecretsId>
</PropertyGroup>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
@ -60,6 +61,7 @@ @@ -60,6 +61,7 @@
<PackageReference Include="Markdig" Version="0.30.4" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">

20
ErsatzTV/OidcHelper.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
namespace ErsatzTV;
public static class OidcHelper
{
public static void Init(IConfiguration configuration)
{
Authority = configuration["OIDC:Authority"];
ClientId = configuration["OIDC:ClientId"];
ClientSecret = configuration["OIDC:ClientSecret"];
IsEnabled = !string.IsNullOrWhiteSpace(Authority) &&
!string.IsNullOrWhiteSpace(ClientId) &&
!string.IsNullOrWhiteSpace(ClientSecret);
}
public static string Authority { get; private set; }
public static string ClientId { get; private set; }
public static string ClientSecret { get; private set; }
public static bool IsEnabled { get; private set; }
}

103
ErsatzTV/Shared/MainLayout.razor

@ -3,8 +3,8 @@ @@ -3,8 +3,8 @@
@using ErsatzTV.Extensions
@using ErsatzTV.Application.Search
@implements IDisposable
@inject NavigationManager _navigationManager
@inject IMediator _mediator
@inject NavigationManager NavigationManager
@inject IMediator Mediator
<MudThemeProvider Theme="_ersatzTvTheme"/>
<MudDialogProvider DisableBackdropClick="true"/>
@ -17,49 +17,51 @@ @@ -17,49 +17,51 @@
<img src="images/ersatztv.png" alt="ErsatzTV"/>
</a>
</div>
<EditForm Model="@_dummyModel" OnSubmit="@(_ => PerformSearch())">
<MudTextField T="string"
@bind-Value="@Query"
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Variant="Variant.Outlined"
Immediate="true"
Class="search-bar"
@onclick="@(() => _isOpen = true)"
OnKeyUp="OnKeyUp">
</MudTextField>
<MudPopover Open="@_isOpen" MaxHeight="300" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter" RelativeWidth="true">
@if (!string.IsNullOrWhiteSpace(_query) && _query.Length >= 3)
{
var matches = _searchTargets.Where(s => s.Name.Contains(_query, StringComparison.CurrentCultureIgnoreCase)).ToList();
if (matches.Any())
<div class="search-form">
<EditForm Model="@_dummyModel" OnSubmit="@(_ => PerformSearch())">
<MudTextField T="string"
@bind-Value="@Query"
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Variant="Variant.Outlined"
Immediate="true"
Class="search-bar"
@onclick="@(() => _isOpen = true)"
OnKeyUp="OnKeyUp">
</MudTextField>
<MudPopover Open="@_isOpen" MaxHeight="300" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter" RelativeWidth="true">
@if (!string.IsNullOrWhiteSpace(_query) && _query.Length >= 3)
{
<MudList Clickable="true" Dense="true">
@foreach (SearchTargetViewModel searchTarget in matches)
{
<MudListItem @key="@searchTarget" OnClick="@(() => NavigateTo(searchTarget))">
<MudText Typo="Typo.body1">@searchTarget.Name</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-disabled">
@(searchTarget.Kind switch
{
SearchTargetKind.Channel => "Channel",
SearchTargetKind.FFmpegProfile => "FFmpeg Profile",
SearchTargetKind.ChannelWatermark => "Channel Watermark",
SearchTargetKind.Collection => "Collection",
SearchTargetKind.MultiCollection => "Multi Collection",
SearchTargetKind.SmartCollection => "Smart Collection",
SearchTargetKind.Schedule => "Schedule",
SearchTargetKind.ScheduleItems => "Schedule Items",
_ => string.Empty
})
</MudText>
</MudListItem>
}
</MudList>
var matches = _searchTargets.Where(s => s.Name.Contains(_query, StringComparison.CurrentCultureIgnoreCase)).ToList();
if (matches.Any())
{
<MudList Clickable="true" Dense="true">
@foreach (SearchTargetViewModel searchTarget in matches)
{
<MudListItem @key="@searchTarget" OnClick="@(() => NavigateTo(searchTarget))">
<MudText Typo="Typo.body1">@searchTarget.Name</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-disabled">
@(searchTarget.Kind switch
{
SearchTargetKind.Channel => "Channel",
SearchTargetKind.FFmpegProfile => "FFmpeg Profile",
SearchTargetKind.ChannelWatermark => "Channel Watermark",
SearchTargetKind.Collection => "Collection",
SearchTargetKind.MultiCollection => "Multi Collection",
SearchTargetKind.SmartCollection => "Smart Collection",
SearchTargetKind.Schedule => "Schedule",
SearchTargetKind.ScheduleItems => "Schedule Items",
_ => string.Empty
})
</MudText>
</MudListItem>
}
</MudList>
}
}
}
</MudPopover>
</EditForm>
</MudPopover>
</EditForm>
</div>
<MudSpacer/>
<MudLink Color="Color.Info" Href="iptv/channels.m3u" Target="_blank" Underline="Underline.None">M3U</MudLink>
<MudLink Color="Color.Info" Href="iptv/xmltv.xml" Target="_blank" Class="mx-4" Underline="Underline.None">XMLTV</MudLink>
@ -73,6 +75,13 @@ @@ -73,6 +75,13 @@
<MudTooltip Text="GitHub">
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Primary" Link="https://github.com/jasongdove/ErsatzTV" Target="_blank"/>
</MudTooltip>
<AuthorizeView>
<form action="/account/logout" method="post">
<MudTooltip Text="Logout">
<MudIconButton Icon="@Icons.Material.Filled.Logout" Color="Color.Secondary" ButtonType="ButtonType.Submit"/>
</MudTooltip>
</form>
</AuthorizeView>
</MudAppBar>
<MudDrawer Open="true" Elevation="2" ClipMode="DrawerClipMode.Always">
<MudNavMenu>
@ -176,17 +185,17 @@ @@ -176,17 +185,17 @@
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
_query = _navigationManager.Uri.GetSearchQuery();
_query = NavigationManager.Uri.GetSearchQuery();
if (_searchTargets is null)
{
_searchTargets = await _mediator.Send(new QuerySearchTargets(), _cts.Token);
_searchTargets = await Mediator.Send(new QuerySearchTargets(), _cts.Token);
}
}
private void PerformSearch()
{
_navigationManager.NavigateTo(_query.GetRelativeSearchQuery(), true);
NavigationManager.NavigateTo(_query.GetRelativeSearchQuery(), true);
StateHasChanged();
}
@ -206,7 +215,7 @@ @@ -206,7 +215,7 @@
private void NavigateTo(SearchTargetViewModel searchTarget) =>
// need to force smart collections to navigate since the query string is all that differs
_navigationManager.NavigateTo(UrlFor(searchTarget), searchTarget.Kind is SearchTargetKind.SmartCollection);
NavigationManager.NavigateTo(UrlFor(searchTarget), searchTarget.Kind is SearchTargetKind.SmartCollection);
private string UrlFor(SearchTargetViewModel searchTarget) =>
searchTarget.Kind switch

67
ErsatzTV/Startup.cs

@ -60,6 +60,7 @@ using FluentValidation.AspNetCore; @@ -60,6 +60,7 @@ using FluentValidation.AspNetCore;
using Ganss.Xss;
using MediatR;
using MediatR.Courier.DependencyInjection;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
@ -118,6 +119,43 @@ public class Startup @@ -118,6 +119,43 @@ public class Startup
#endif
});
OidcHelper.Init(Configuration);
if (OidcHelper.IsEnabled)
{
services.AddAuthentication(
options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie", options =>
{
options.CookieManager = new ChunkingCookieManager();
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(
"oidc",
options =>
{
options.Authority = OidcHelper.Authority;
options.ClientId = OidcHelper.ClientId;
options.ClientSecret = OidcHelper.ClientSecret;
options.ResponseType = "code";
options.UsePkce = true;
options.ResponseMode = "query";
options.SaveTokens = true;
options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
});
}
services.AddCors(
o => o.AddPolicy(
"AllowAll",
@ -149,14 +187,23 @@ public class Startup @@ -149,14 +187,23 @@ public class Startup
services.AddFluentValidationAutoValidation();
services.AddValidatorsFromAssemblyContaining<Startup>();
if (!CurrentEnvironment.IsDevelopment())
string v2 = Environment.GetEnvironmentVariable("ETV_UI_V2");
if (!CurrentEnvironment.IsDevelopment() && !string.IsNullOrWhiteSpace(v2))
{
services.AddSpaStaticFiles(options => options.RootPath = "wwwroot/v2");
}
services.AddMemoryCache();
services.AddRazorPages();
services.AddRazorPages(
options =>
{
if (OidcHelper.IsEnabled)
{
options.Conventions.AuthorizeFolder("/");
}
});
services.AddServerSideBlazor();
services.AddMudServices();
@ -317,8 +364,15 @@ public class Startup @@ -317,8 +364,15 @@ public class Startup
});
app.UseRouting();
if (OidcHelper.IsEnabled)
{
app.UseAuthentication();
app.UseAuthorization();
}
if (!env.IsDevelopment())
string v2 = Environment.GetEnvironmentVariable("ETV_UI_V2");
if (!env.IsDevelopment() && !string.IsNullOrWhiteSpace(v2))
{
app.Map(
"/v2",
@ -330,6 +384,13 @@ public class Startup @@ -330,6 +384,13 @@ public class Startup
}
app2.UseRouting();
if (OidcHelper.IsEnabled)
{
app.UseAuthentication();
app.UseAuthorization();
}
app2.UseEndpoints(e => e.MapFallbackToFile("index.html"));
app2.UseFileServer(
new FileServerOptions

2
ErsatzTV/wwwroot/css/site.css

@ -74,7 +74,7 @@ @@ -74,7 +74,7 @@
border-radius: 4px;
}
.app-bar form { flex-grow: 1; }
.app-bar .search-form { flex-grow: 1; }
.fanart-container {
position: relative;

Loading…
Cancel
Save