Files
timmy b20d993f33 Updates
Use a consistent http and oidc clients
Use the access token to send an a authenticated request to retrieve avatar and do it at the same time as requesting the userinfo endpoint
Merge JTokens instead of re-parsing json strings to merge configuration
2026-08-22 16:59:28 -07:00

145 lines
6.0 KiB
C#

namespace Jellyfin.Plugin.OIDC_Auth;
using Logging = Microsoft.Extensions.Logging;
/// <summary>
/// The OIDC plugin class.
/// </summary>
public class OIDCPlugin : MediaBrowser.Common.Plugins.BasePlugin<Jellyfin.Plugin.OIDC_Auth.PluginConfiguration>,
MediaBrowser.Common.Plugins.IPlugin, MediaBrowser.Model.Plugins.IHasWebPages {
/// <summary>
/// Initializes a new instance of the <see cref="OIDCPlugin" /> class.
/// </summary>
/// <param name="applicationPaths">Internal Jellyfin interface for the ApplicationPath.</param>
/// <param name="xmlSerializer">Internal Jellyfin interface for the XML information.</param>
/// <param name="logger">Fuck C#.</param>
public OIDCPlugin(
MediaBrowser.Common.Configuration.IApplicationPaths applicationPaths,
MediaBrowser.Model.Serialization.IXmlSerializer xmlSerializer,
Logging.ILogger<OIDCPlugin> logger
) : base(applicationPaths, xmlSerializer) {
if (OIDCPlugin.Instance == null) {
OIDCPlugin.Instance = this;
foreach (var config in this.Configuration.Configs) {
config.Value.FixCanonicalLinks();
}
}
if (string.IsNullOrWhiteSpace(OIDCPlugin.IndexHTML)) {
var indexPath = System.IO.Path.Combine(applicationPaths.WebPath, "index.html");
string content = System.IO.File.ReadAllText(indexPath);
content = content.Substring(0, content.IndexOf("</head>"));
// See GetHTMLPage for how this is used
OIDCPlugin.IndexHTML = content;
}
}
/// <summary>
/// Gets the instance of the OIDC plugin.
/// </summary>
public static OIDCPlugin Instance { get; private set; }
/// <summary>
/// Gets index HTML page.
/// </summary>
public static string IndexHTML { get; private set; }
/// <summary>
/// Gets the name of the OIDC plugin.
/// </summary>
public override string Name => "OIDC Authentication";
/// <summary>
/// Gets the GUID of the OIDC plugin.
/// </summary>
public override System.Guid Id => System.Guid.Parse("c2e83d4a-1a92-11f1-a789-f84d896c963f");
/// <summary>
/// Returns html page with content injected into it.
/// </summary>
/// <param name="content">The content to inject.</param>
/// <param name="baseURL">The base url so that JellyFin react app can load.</param>
/// <returns>A list of internal webpages in this application.</returns>
public string GetHTMLPage(string content, string baseURL) {
string pre = """
<body class="force-scroll dashboardDocument mouseIdle">
<div id="reactRoot" style="display: none;"></div>
""";
// loads jellyfin so that styles get set appropriately. ApiClient will not get set if we are not logged in
// Make sure jellyfin doesnt show. But we still need reactRoot in order for jellyfin to initialize.
string post = """
<script>
function waitForWindowProperty(propName, callback, interval = 100) {
const timer = setInterval(() => {
if (window[propName] !== undefined) {
clearInterval(timer);
callback(window[propName]);
}
}, interval);
}
waitForWindowProperty("ApiClient", () => {
console.log("ApiClientFound");
document.dispatchEvent(new CustomEvent("ApiClientFound", { }));
});
</script>
</body></html>
"""; // Jellyfin-web doesn't have a way to inject public html pages otherwise none of this would be needed
return OIDCPlugin.IndexHTML
.Replace("<head>", $"<head><base href=\"{baseURL.TrimEnd('/')}/web/\">") // Set the base meta property so that includes are resolved
+ pre + content + post;
}
/// <summary>
/// Returns the available internal web pages of this plugin.
/// </summary>
/// <returns>A list of internal webpages in this application.</returns>
public System.Collections.Generic.IEnumerable<MediaBrowser.Model.Plugins.PluginPageInfo> GetPages() {
return [
new() {
Name = "OIDC-Auth-config.html",
EmbeddedResourcePath = $"{this.GetType().Namespace}.Assets.OIDC-Auth-config.html",
EnableInMainMenu = true
},
new() {
Name = "OIDC-Auth-config.js",
EmbeddedResourcePath = $"{this.GetType().Namespace}.Assets.OIDC-Auth-config.js"
},
new() {
Name = "OIDC-Auth-config.css",
EmbeddedResourcePath = $"{this.GetType().Namespace}.Assets.OIDC-Auth-config.css"
},
new() {
Name = "OIDC-Auth-linking.html",
EmbeddedResourcePath = $"{this.GetType().Namespace}.Assets.OIDC-Auth-linking.html"
},
new() {
Name = "OIDC-Auth-linking.js",
EmbeddedResourcePath = $"{this.GetType().Namespace}.Assets.OIDC-Auth-linking.js"
},
new() {
Name = "OIDC-Auth-auth.html",
EmbeddedResourcePath = $"{this.GetType().Namespace}.Assets.OIDC-Auth-auth.html"
}
];
}
/// <summary>
/// Returns the content of a page embedded in the plugin.
/// </summary>
/// <param name="name">The name of the file to extract from the plugin.</param>
/// <returns>A list of internal webpages in this application.</returns>
public string GetPage(string name) {
foreach (MediaBrowser.Model.Plugins.PluginPageInfo pi in this.GetPages()) {
if (pi.Name != "OIDC-Auth-" + name) {
continue;
}
using (var s = OIDCPlugin.Instance.GetType().Assembly.GetManifestResourceStream(pi.EmbeddedResourcePath)) {
if (s == null) {
break;
}
return new System.IO.StreamReader(s).ReadToEnd();
}
}
return string.Empty;
}
}