mirror of
https://github.com/9p4/jellyfin-plugin-sso.git
synced 2026-09-19 13:12:19 +00:00
Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ad72eb955 | ||
|
|
4ee785577e | ||
|
|
ecf943410b | ||
|
|
14fac85681 | ||
|
|
23cce83012 | ||
|
|
103ab715b3 | ||
|
|
e45e97a5cd | ||
|
|
021099e238 | ||
|
|
f8a3b5b0bd | ||
|
|
2b4b9b7050 | ||
|
|
13618d6643 | ||
|
|
1871791501 | ||
|
|
5714ef45d3 | ||
|
|
99d4f1e69a | ||
|
|
85e172638f | ||
|
|
f359086cf3 | ||
|
|
33afc0526a | ||
|
|
36aa880601 | ||
|
|
fbd0b83323 | ||
|
|
867191f0ac | ||
|
|
3054a0935e | ||
|
|
2b4e25a83c | ||
|
|
32eeecb65b | ||
|
|
e88656ae38 | ||
|
|
3f9f465e8f | ||
|
|
27bd2e99b5 | ||
|
|
f8e864f84a | ||
|
|
7362f987c2 | ||
|
|
7d14558622 | ||
|
|
af5332d457 | ||
|
|
7f37208ae6 | ||
|
|
3f701ef420 | ||
|
|
ed1a2617a8 | ||
|
|
7becc4f616 | ||
|
|
11a2354951 | ||
|
|
4959f55e5f | ||
|
|
3c50472480 | ||
|
|
d4d883297e | ||
|
|
9ee87a6b13 | ||
|
|
71d70154a7 | ||
|
|
6807ee87dc | ||
|
|
256daf1dd5 | ||
|
|
5164281288 | ||
|
|
853beef3ca | ||
|
|
541b676a4a | ||
|
|
5dc66d0ce3 | ||
|
|
3893f6a8b1 | ||
|
|
6fb1cf7446 | ||
|
|
ceec9b8ab5 | ||
|
|
6f307feebd | ||
|
|
266030c0d9 | ||
|
|
421b0a9008 | ||
|
|
5302d79240 | ||
|
|
649f0305f5 | ||
|
|
c7996c7fa3 | ||
|
|
ea1527496b | ||
|
|
c11478425e | ||
|
|
facbe3bf31 | ||
|
|
69ffd48864 | ||
|
|
f375b97013 | ||
|
|
8cc06bff0e | ||
|
|
db92a04d58 | ||
|
|
752809b0e6 | ||
|
|
f25d789744 | ||
|
|
5f18d65a11 | ||
|
|
e89a59af00 | ||
|
|
ebaf044812 | ||
|
|
b24f5bae75 | ||
|
|
96657796ed | ||
|
|
d94aa59dae | ||
|
|
6fc0e98f7c | ||
|
|
bae0dddcb4 | ||
|
|
8385f2fde5 | ||
|
|
5324a3e929 | ||
|
|
776049e5f1 | ||
|
|
6c9cc69af7 | ||
|
|
21048e4bb2 | ||
|
|
c2d620a9ac | ||
|
|
a0ffa1fea9 | ||
|
|
24921ec0e6 | ||
|
|
73761dc5af | ||
|
|
a1679fbe6c | ||
|
|
28b4ac5869 | ||
|
|
c6b0b6a14f | ||
|
|
f8b7f1ff88 | ||
|
|
0c807ddc20 | ||
|
|
be1494d0ad | ||
|
|
47ba8d8bf8 | ||
|
|
e215f015d2 | ||
|
|
e8894dd962 | ||
|
|
db31e39c10 | ||
|
|
56aa6497b6 | ||
|
|
86cc1f9a6f | ||
|
|
107a848e5d | ||
|
|
10984c1174 | ||
|
|
cf4f30774e | ||
|
|
34344b5964 | ||
|
|
063fe40534 | ||
|
|
19700ddbf8 | ||
|
|
b886d31a6f | ||
|
|
e9798bbe68 | ||
|
|
fee817743d | ||
|
|
47082fac74 | ||
|
|
5470fe6f92 | ||
|
|
895dde0740 | ||
|
|
f8e38580c4 | ||
|
|
6250850579 | ||
|
|
6bc2b33546 | ||
|
|
f35254e7e4 | ||
|
|
e4f7076f6e | ||
|
|
5b0106969d | ||
|
|
b146553631 | ||
|
|
b03ef43cc7 | ||
|
|
7024441cb6 | ||
|
|
8a176000ec | ||
|
|
b8439bc710 | ||
|
|
718b75eb9e |
-454
@@ -1,454 +0,0 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# Mac bundle stuff
|
||||
*.dmg
|
||||
*.app
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
##
|
||||
## Visual Studio Code
|
||||
##
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
-674
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,172 +0,0 @@
|
||||
# Jellyfin SSO Plugin
|
||||
|
||||
This plugin allows users to sign in through an SSO provider (such as Google, Facebook, or your own provider). This enables one-click signin.
|
||||
|
||||
https://user-images.githubusercontent.com/17993169/149681516-f93b43f5-fa5c-4c1f-a909-e5414878a864.mp4
|
||||
|
||||
## Current State:
|
||||
|
||||
This is 100% alpha software! PRs are welcome to improve the code.
|
||||
|
||||
There is NO admin configuration! You must use the API to configure the program!
|
||||
|
||||
**This is for Jellyfin 10.8**
|
||||
|
||||
## Tested Providers
|
||||
|
||||
- Google OpenID: Works, but usernames are all numeric
|
||||
- Keycloak OpenID and SAML: Works
|
||||
|
||||
## Supported Protocols
|
||||
|
||||
- [OpenID](https://openid.net/what-is-openid/)
|
||||
- [SAML](https://www.cloudflare.com/learning/access-management/what-is-saml/)
|
||||
|
||||
## Security
|
||||
|
||||
This is my first time writing C# so please take all of the code written here with a grain of salt. This program should be reasonably secure since it validates all information passed from the client with either a certificate or a secret internal state.
|
||||
|
||||
## Installing
|
||||
|
||||
Add the package repo [https://repo.saggis.com/jellyfin/manifest.json](https://repo.saggis.com/jellyfin/manifest.json) to your Jellyfin configuration. Then, install the package!
|
||||
|
||||
## Building
|
||||
|
||||
This is built with .NET 6.0. Build with `dotnet publish .` for the debug release in the `SSO-Auth` directory. Copy over the `IdentityModel.OidcClient.dll`, the `IdentityModel.dll` and the `SSO-Auth.dll` files in the `/bin/Debug/net6.0/publish` directory to a new folder in your Jellyfin configuration: `config/plugins/sso`.
|
||||
|
||||
## Releasing
|
||||
|
||||
This plugin uses [JPRM](https://github.com/oddstr13/jellyfin-plugin-repository-manager) to build the plugin. Refer to the documentation there to install JPRM.
|
||||
|
||||
Build the zipped plugin with `jprm --verbosity=debug plugin build .`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Admin page
|
||||
- [ ] Automated tests
|
||||
- [x] Add role/claims support
|
||||
- [ ] Use canonical usernames instead of preferred usernames
|
||||
|
||||
## Examples
|
||||
|
||||
### SAML
|
||||
|
||||
Example for adding a SAML configuration with the API using [curl](https://curl.se/):
|
||||
|
||||
`curl -v -X POST -H "Content-Type: application/json" -d '{"samlEndpoint": "https://keycloak.example.com/realms/test/protocol/saml", "samlClientId": "jellyfin-saml", "samlCertificate": "Very long base64 encoded string here", "enabled": true, "enableAllFolders": true, "enabledFolders": ["folder1", "folder2"], "adminRoles": [], "roles": []}' "https://myjellyfin.example.com/sso/SAML/Add?api_key=API_KEY_HERE"`
|
||||
|
||||
Make sure that the JSON is the same as the configuration you would like.
|
||||
|
||||
The SAML provider must have the following configuration (I am using Keycloak, and I cannot speak for whatever you will see):
|
||||
|
||||
- Sign Documents on
|
||||
- Sign Assertions off
|
||||
- Client Signature Required off
|
||||
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/OID/p/clientid)
|
||||
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
|
||||
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/SAML/p/clientid)
|
||||
|
||||
Make sure that `clientid` is replaced with the actual client ID!
|
||||
|
||||
### OpenID
|
||||
|
||||
Example for adding an OpenID configuration with the API using [curl](https://curl.se/)
|
||||
|
||||
`curl -v -X POST -H "Content-Type: application/json" -d '{"oidEndpoint": "https://keycloak.example.com/realms/test", "oidClientId": "jellyfin-oid", "oidSecret": "short secret here", "enabled": true, "enableAllFolders": true, "enabledFolders": ["folder3", "folder4"], "adminRoles": [], "roles": []}' "https://myjellyfin.example.com/sso/OID/Add?api_key=API_KEY_HERE"`
|
||||
|
||||
The OpenID provider must have the following configuration (again, I am using Keycloak)
|
||||
|
||||
- Access Type: Confidential
|
||||
- Standard Flow Enabled
|
||||
- Redirect URI: [https://myjellyfin.example.com/sso/OID/r/clientid](https://myjellyfin.example.com/sso/OID/r/clientid)
|
||||
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
|
||||
|
||||
Make sure that `clientid` is replaced with the actual client ID!
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The API is all done from a base URL of `/sso/`
|
||||
|
||||
### SAML
|
||||
|
||||
#### Flow
|
||||
|
||||
- POST `SAML/p/clientid`: This is the SAML POST endpoint. It accepts a form response from the SAML provider and returns HTML and JavaScript for the client to login.
|
||||
- GET `SAML/p/clientid`: This is the SAML initiator: it will begin the authorization flow for SAML with a given client ID.
|
||||
- POST `SAML/Auth`: This is the SAML client-side API: the HTML and JavaScript client will call this endpoint to receive Jellyfin credentials. Post format is in JSON with the following keys:
|
||||
- `deviceId`: string. Device ID.
|
||||
- `deviceName`: string. Device name.
|
||||
- `appName`: string. App name.
|
||||
- `appVersion`: string. App version.
|
||||
- `data`: string. The signed SAML XML request. Used to verify a request.
|
||||
- `provider`: string. The current SAML client ID.
|
||||
|
||||
#### Configuration
|
||||
|
||||
These all require authorization. Append an API key to the end of the request: `curl "http://myjellyfin.example.com/sso/SAML/Get?api_key=API_KEY_HERE"`
|
||||
|
||||
- POST `SAML/Add`: This adds a configuration for SAML. It accepts JSON with the following keys and format:
|
||||
- `samlEndpoint`: string. The SAML endpoint.
|
||||
- `samlClientId`: string. The SAML client ID.
|
||||
- `samlCertificate`: string. The base64 encoded SAML certificate.
|
||||
- `enabled`: boolean. Determines if the provider is enabled or not.
|
||||
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
|
||||
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
|
||||
- `roles`: array of strings. This validates the SAML response against the `Role` attribute. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking.
|
||||
- `adminRoles`: array of strings. This uses SAML response's `Role` attributes. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
|
||||
- GET `SAML/Del/clientId`: This removes a configuration for SAML for a given client ID.
|
||||
- GET `SAML/Get`: Lists the configurations currently available.
|
||||
|
||||
|
||||
### OpenID
|
||||
|
||||
#### Flow
|
||||
|
||||
- GET `OID/r/clientId`: This is the OpenID callback path. This will return HTML and JavaScript for the client to login.
|
||||
- GET `OID/p/clientId`: This is the OpenID initiator: it will begin the authorization flow for OpenID with a given client ID.
|
||||
- POST `OID/Auth`: This is the OpenID client-side API: the HTML and JavaScript client will call this endpoint to receive Jellyfin credentials. Post format is in JSON with the following keys:
|
||||
- `deviceId`: string. Device ID.
|
||||
- `deviceName`: string. Device name.
|
||||
- `appName`: string. App name.
|
||||
- `appVersion`: string. App version.
|
||||
- `data`: string. The OpenID state. Used to verify a request.
|
||||
- `provider`: string. The current OpenID client ID.
|
||||
|
||||
#### Configuration
|
||||
|
||||
These all require authorization. Append an API key to the end of the request: `curl "http://myjellyfin.example.com/sso/OID/Get?api_key=9c6e5fae4ae145669e6b7a3942f813b7"`
|
||||
|
||||
- POST `OID/Add`: This adds a configuration for OpenID. It accepts JSON with the following keys and format:
|
||||
- `oidEndpoint`: string. The OpenID endpoint. Must have a `.well-known` path available.
|
||||
- `oidClientId`: string. The OpenID client ID.
|
||||
- `oidSecret`: string. The OpenID secret.
|
||||
- `enabled`: boolean. Determines if the provider is enabled or not.
|
||||
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
|
||||
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
|
||||
- `roles`: array of strings. This validates the OpenID response against the `realm_access` claim. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking. This currently only works for Keycloak (to my knowledge).
|
||||
- `adminRoles`: array of strings. This uses the OpenID response against the `realm_access` claim. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
|
||||
- GET `OID/Del/clientId`: This removes a configuration for OpenID for a given client ID.
|
||||
- GET `OID/Get`: Lists the configurations currently available.
|
||||
- GET `OID/States`: Lists currently active OpenID flows in progress.
|
||||
|
||||
## Limitations
|
||||
|
||||
There is no GUI to sign in. You have to make it yourself! The buttons should redirect to something like this: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/SAML/p/clientid) replacing `clientid` with the provider client ID and `SAML` with the auth scheme (either `SAML` or `OID`).
|
||||
|
||||
Furthermore, there is no functional admin page (yet). PRs for this are welcome. In the meantime, you have to interact with the API to add or remove configurations.
|
||||
|
||||
There is also no logout callback. Logging out of Jellyfin will log you out of Jellyfin only, instead of the SSO provider as well.
|
||||
|
||||
~~This only supports Jellyfin on it's own domain (for now). This is because I'm using string concatenation for generating some URLs. A PR is welcome to patch this.~~ Fixed in [PR #1](https://github.com/9p4/jellyfin-plugin-sso/pull/1).
|
||||
|
||||
**This only works on the web UI**. The user must open the Jellyfin web UI BEFORE using the SSO program to populate some values in the localStorage.
|
||||
|
||||
## Credits and Thanks
|
||||
|
||||
Much thanks to the [Jellyfin LDAP plugin](https://github.com/jellyfin/jellyfin-plugin-ldapauth) for offering a base for me to start on my plugin.
|
||||
|
||||
I use the [AspNet SAML](https://github.com/jitbit/AspNetSaml/) library for the SAML side of things (patched to work with Base64 on non-Windows machines).
|
||||
|
||||
I use the [IdentityModel OIDC Client](https://github.com/IdentityModel/IdentityModel.OidcClient/) library for the OpenID side of things.
|
||||
|
||||
Thanks to these projects, without which I would have been pulling my hair out implementing these protocols from scratch.
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSO-Auth", "SSO-Auth\SSO-Auth.csproj", "{C30A5CFB-B27E-4E83-9E96-1E0362B36748}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C30A5CFB-B27E-4E83-9E96-1E0362B36748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C30A5CFB-B27E-4E83-9E96-1E0362B36748}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C30A5CFB-B27E-4E83-9E96-1E0362B36748}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C30A5CFB-B27E-4E83-9E96-1E0362B36748}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,450 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel.Client;
|
||||
using IdentityModel.OidcClient;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.SSO_Auth.Config;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.SSO_Auth.Api;
|
||||
|
||||
/// <summary>
|
||||
/// The sso api controller.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class SSOController : ControllerBase
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ILogger<SSOController> _logger;
|
||||
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SSOController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{SSOController}"/> interface.</param>
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
public SSOController(ILogger<SSOController> logger, ISessionManager sessionManager, IUserManager userManager)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_logger.LogInformation("SSO Controller initialized");
|
||||
}
|
||||
|
||||
[HttpGet("OID/r/{provider}")]
|
||||
public ActionResult OIDPost(string provider)
|
||||
{
|
||||
// Actually a GET: https://github.com/IdentityModel/IdentityModel.OidcClient/issues/325
|
||||
foreach (var config in SSOPlugin.Instance.Configuration.OIDConfigs)
|
||||
{
|
||||
if (config.OIDClientId == provider && config.Enabled)
|
||||
{
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = config.OIDEndpoint,
|
||||
ClientId = config.OIDClientId,
|
||||
ClientSecret = config.OIDSecret,
|
||||
RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
|
||||
Scope = "openid profile",
|
||||
};
|
||||
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
|
||||
var oidcClient = new OidcClient(options);
|
||||
var state = StateManager[Request.Query["state"]].State;
|
||||
var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, state).Result;
|
||||
if (result.IsError)
|
||||
{
|
||||
return Content("Something went wrong...", MediaTypeNames.Text.Plain);
|
||||
}
|
||||
|
||||
foreach (var claim in result.User.Claims)
|
||||
{
|
||||
if (claim.Type == "preferred_username")
|
||||
{
|
||||
StateManager[Request.Query["state"]].Username = claim.Value;
|
||||
if (config.Roles.Length == 0)
|
||||
{
|
||||
StateManager[Request.Query["state"]].Valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if allowed to login based on realm roles
|
||||
if (config.Roles.Length != 0)
|
||||
{
|
||||
if (claim.Type == "realm_access") // This is specific to Keycloak. Don't use roles without Keycloak, I guess
|
||||
{
|
||||
List<string> roles = JsonConvert.DeserializeObject<IDictionary<string, List<string>>>(claim.Value)["roles"]; // Might need error handling here
|
||||
foreach (string validRoles in config.Roles)
|
||||
{
|
||||
foreach (string role in roles)
|
||||
{
|
||||
if (role.Equals(validRoles))
|
||||
{
|
||||
StateManager[Request.Query["state"]].Valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if admin
|
||||
if (config.AdminRoles.Length != 0)
|
||||
{
|
||||
if (claim.Type == "realm_access") // This is specific to Keycloak. Don't use roles without Keycloak, I guess
|
||||
{
|
||||
List<string> roles = JsonConvert.DeserializeObject<IDictionary<string, List<string>>>(claim.Value)["roles"]; // Might need error handling here
|
||||
foreach (string validAdminRoles in config.AdminRoles)
|
||||
{
|
||||
foreach (string role in roles)
|
||||
{
|
||||
if (role.Equals(validAdminRoles))
|
||||
{
|
||||
StateManager[Request.Query["state"]].Admin = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the provider doesn't support preferred_username, then use sub
|
||||
if (!StateManager[Request.Query["state"]].Valid)
|
||||
{
|
||||
foreach (var claim in result.User.Claims)
|
||||
{
|
||||
if (claim.Type == "sub")
|
||||
{
|
||||
StateManager[Request.Query["state"]].Username = claim.Value;
|
||||
if (config.Roles.Length == 0)
|
||||
{
|
||||
StateManager[Request.Query["state"]].Valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StateManager[Request.Query["state"]].Valid)
|
||||
{
|
||||
return Content(WebResponse.OIDGenerator(data: Request.Query["state"], provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Content("Error. Check permissions."); // TODO: Return error code as well
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Content("no active providers found"); // TODO: Return error code as well
|
||||
}
|
||||
|
||||
[HttpGet("OID/p/{provider}")]
|
||||
public async Task<ActionResult> OIDChallenge(string provider)
|
||||
{
|
||||
Invalidate();
|
||||
foreach (var config in SSOPlugin.Instance.Configuration.OIDConfigs)
|
||||
{
|
||||
if (config.OIDClientId == provider && config.Enabled)
|
||||
{
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = config.OIDEndpoint,
|
||||
ClientId = config.OIDClientId,
|
||||
ClientSecret = config.OIDSecret,
|
||||
RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
|
||||
Scope = "openid profile"
|
||||
};
|
||||
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
|
||||
var oidcClient = new OidcClient(options);
|
||||
var state = await oidcClient.PrepareLoginAsync().ConfigureAwait(false);
|
||||
StateManager.Add(state.State, new TimedAuthorizeState(state, DateTime.Now));
|
||||
return Redirect(state.StartUrl);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Provider does not exist");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequiresElevation")]
|
||||
[HttpPost("OID/Add")]
|
||||
public void OIDAdd([FromBody] OIDConfig oidConfig)
|
||||
{
|
||||
var configuration = SSOPlugin.Instance.Configuration;
|
||||
for (var i = 0; i < configuration.OIDConfigs.Count; i++)
|
||||
{
|
||||
if (configuration.OIDConfigs[i].OIDClientId.Equals(oidConfig.OIDClientId))
|
||||
{
|
||||
configuration.OIDConfigs.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
configuration.OIDConfigs.Add(oidConfig);
|
||||
SSOPlugin.Instance.UpdateConfiguration(configuration);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequiresElevation")]
|
||||
[HttpGet("OID/Del/{provider}")]
|
||||
public void OIDDel(string provider)
|
||||
{
|
||||
var configuration = SSOPlugin.Instance.Configuration;
|
||||
for (var i = 0; i < configuration.OIDConfigs.Count; i++)
|
||||
{
|
||||
if (configuration.OIDConfigs[i].OIDClientId.Equals(provider))
|
||||
{
|
||||
configuration.OIDConfigs.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
SSOPlugin.Instance.UpdateConfiguration(configuration);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequiresElevation")]
|
||||
[HttpGet("OID/Get")]
|
||||
public ActionResult OIDProviders()
|
||||
{
|
||||
return Ok(SSOPlugin.Instance.Configuration.OIDConfigs);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequiresElevation")]
|
||||
[HttpGet("OID/States")]
|
||||
public ActionResult OIDStates()
|
||||
{
|
||||
return Ok(StateManager);
|
||||
}
|
||||
|
||||
[HttpPost("OID/Auth")]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
public async Task<ActionResult> OIDAuth([FromBody] AuthResponse response)
|
||||
{
|
||||
foreach (var oidConfig in SSOPlugin.Instance.Configuration.OIDConfigs)
|
||||
{
|
||||
if (oidConfig.OIDClientId == response.Provider && oidConfig.Enabled)
|
||||
{
|
||||
foreach (var kvp in StateManager)
|
||||
{
|
||||
if (kvp.Value.State.State.Equals(response.Data) && kvp.Value.Valid)
|
||||
{
|
||||
var authenticationResult = await Authenticate(kvp.Value.Username, kvp.Value.Admin, oidConfig.EnableAllFolders, oidConfig.EnabledFolders, response)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(authenticationResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Problem("Something went wrong");
|
||||
}
|
||||
|
||||
[HttpPost("SAML/p/{provider}")]
|
||||
public ActionResult SAMLPost(string provider)
|
||||
{
|
||||
// I'm sure there's a better way than using nested for loops but eh whatever
|
||||
foreach (var samlConfig in SSOPlugin.Instance.Configuration.SamlConfigs)
|
||||
{
|
||||
if (samlConfig.SamlClientId == provider && samlConfig.Enabled)
|
||||
{
|
||||
var samlResponse = new Response(samlConfig.SamlCertificate, Request.Form["SAMLResponse"]);
|
||||
if (samlConfig.Roles.Length == 0)
|
||||
{
|
||||
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
|
||||
}
|
||||
foreach (string role in samlResponse.GetCustomAttributes("Role"))
|
||||
{
|
||||
foreach (string allowedRole in samlConfig.Roles)
|
||||
{
|
||||
if (allowedRole.Equals(role))
|
||||
{
|
||||
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Content("401 Forbidden");
|
||||
}
|
||||
}
|
||||
|
||||
return Content("no active providers found"); // TODO: Return error code as well
|
||||
}
|
||||
|
||||
[HttpGet("SAML/p/{provider}")]
|
||||
public RedirectResult SAMLChallenge(string provider)
|
||||
{
|
||||
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
|
||||
{
|
||||
if (config.SamlClientId == provider && config.Enabled)
|
||||
{
|
||||
var request = new AuthRequest(
|
||||
config.SamlClientId,
|
||||
GetRequestBase() + "/sso/SAML/p/" + provider);
|
||||
|
||||
return Redirect(request.GetRedirectUrl(config.SamlEndpoint));
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Provider does not exist");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequiresElevation")]
|
||||
[HttpPost("SAML/Add")]
|
||||
public void SamlAdd([FromBody] SamlConfig samlConfig)
|
||||
{
|
||||
var configuration = SSOPlugin.Instance.Configuration;
|
||||
for (var i = 0; i < configuration.SamlConfigs.Count; i++)
|
||||
{
|
||||
if (configuration.SamlConfigs[i].SamlClientId.Equals(samlConfig.SamlClientId))
|
||||
{
|
||||
configuration.SamlConfigs.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
configuration.SamlConfigs.Add(samlConfig);
|
||||
SSOPlugin.Instance.UpdateConfiguration(configuration);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequiresElevation")]
|
||||
[HttpGet("SAML/Del/{provider}")]
|
||||
public void SamlDel(string provider)
|
||||
{
|
||||
var configuration = SSOPlugin.Instance.Configuration;
|
||||
for (var i = 0; i < configuration.SamlConfigs.Count; i++)
|
||||
{
|
||||
if (configuration.SamlConfigs[i].SamlClientId.Equals(provider))
|
||||
{
|
||||
configuration.SamlConfigs.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
SSOPlugin.Instance.UpdateConfiguration(configuration);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequiresElevation")]
|
||||
[HttpGet("SAML/Get")]
|
||||
public ActionResult SamlProviders()
|
||||
{
|
||||
return Ok(SSOPlugin.Instance.Configuration.SamlConfigs);
|
||||
}
|
||||
|
||||
[HttpPost("SAML/Auth")]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
public async Task<ActionResult> SamlAuth([FromBody] AuthResponse response)
|
||||
{
|
||||
foreach (var samlConfig in SSOPlugin.Instance.Configuration.SamlConfigs)
|
||||
{
|
||||
if (samlConfig.SamlClientId == response.Provider && samlConfig.Enabled)
|
||||
{
|
||||
bool isAdmin = false;
|
||||
var samlResponse = new Response(samlConfig.SamlCertificate, response.Data);
|
||||
foreach (string role in samlResponse.GetCustomAttributes("Role"))
|
||||
{
|
||||
foreach (string allowedRole in samlConfig.AdminRoles)
|
||||
{
|
||||
if (allowedRole.Equals(role))
|
||||
{
|
||||
isAdmin = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
var authenticationResult = await Authenticate(samlResponse.GetNameID(), isAdmin, samlConfig.EnableAllFolders, samlConfig.EnabledFolders, response)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(authenticationResult);
|
||||
}
|
||||
}
|
||||
|
||||
return Problem("Something went wrong");
|
||||
}
|
||||
|
||||
private async Task<AuthenticationResult> Authenticate(string username, bool isAdmin, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
|
||||
{
|
||||
User user = null;
|
||||
user = _userManager.GetUserByName(username);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("SSO user doesn't exist, creating...");
|
||||
user = await _userManager.CreateUserAsync(username).ConfigureAwait(false);
|
||||
}
|
||||
user.AuthenticationProviderId = GetType().FullName;
|
||||
user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
||||
user.SetPermission(PermissionKind.EnableAllFolders, enableAllFolders);
|
||||
if (!enableAllFolders)
|
||||
{
|
||||
user.SetPreference(PreferenceKind.EnabledFolders, enabledFolders);
|
||||
}
|
||||
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
var authRequest = new AuthenticationRequest();
|
||||
authRequest.UserId = user.Id;
|
||||
authRequest.Username = user.Username;
|
||||
authRequest.App = authResponse.AppName;
|
||||
authRequest.AppVersion = authResponse.AppVersion;
|
||||
authRequest.DeviceId = authResponse.DeviceID;
|
||||
authRequest.DeviceName = authResponse.DeviceName;
|
||||
_logger.LogInformation("Auth request created...");
|
||||
return await _sessionManager.AuthenticateDirect(authRequest).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void Invalidate()
|
||||
{
|
||||
foreach (var kvp in StateManager)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
if (now.Subtract(kvp.Value.Created).TotalMinutes > 1)
|
||||
{
|
||||
StateManager.Remove(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRequestBase()
|
||||
{
|
||||
return Request.Scheme + "://" + Request.Host + Request.PathBase;
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthResponse
|
||||
{
|
||||
public string DeviceID { get; set; }
|
||||
|
||||
public string DeviceName { get; set; }
|
||||
|
||||
public string AppName { get; set; }
|
||||
|
||||
public string AppVersion { get; set; }
|
||||
|
||||
public string Data { get; set; }
|
||||
|
||||
public string Provider { get; set; }
|
||||
}
|
||||
|
||||
public class TimedAuthorizeState
|
||||
{
|
||||
public TimedAuthorizeState(AuthorizeState state, DateTime created)
|
||||
{
|
||||
State = state;
|
||||
Created = created;
|
||||
Valid = false;
|
||||
Admin = false;
|
||||
}
|
||||
|
||||
public AuthorizeState State { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
public bool Valid { get; set; }
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
public bool Admin { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SSO_Auth.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin Configuration.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
SamlConfigs = new List<SamlConfig>();
|
||||
OIDConfigs = new List<OIDConfig>();
|
||||
}
|
||||
|
||||
[XmlArray("SamlConfigs")]
|
||||
[XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")]
|
||||
public List<SamlConfig> SamlConfigs { get; set; }
|
||||
|
||||
[XmlArray("OIDConfigs")]
|
||||
[XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")]
|
||||
public List<OIDConfig> OIDConfigs { get; set; }
|
||||
}
|
||||
|
||||
[XmlRoot("PluginConfiguration")]
|
||||
public class SamlConfig
|
||||
{
|
||||
public string SamlEndpoint { get; set; }
|
||||
|
||||
public string SamlClientId { get; set; }
|
||||
|
||||
public string SamlCertificate { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public bool EnableAllFolders { get; set; }
|
||||
|
||||
public string[] EnabledFolders { get; set; }
|
||||
|
||||
public string[] AdminRoles { get; set; }
|
||||
|
||||
public string[] Roles { get; set; }
|
||||
}
|
||||
|
||||
[XmlRoot("PluginConfiguration")]
|
||||
public class OIDConfig
|
||||
{
|
||||
public string OIDEndpoint { get; set; }
|
||||
|
||||
public string OIDClientId { get; set; }
|
||||
|
||||
public string OIDSecret { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public bool EnableAllFolders { get; set; }
|
||||
|
||||
public string[] EnabledFolders { get; set; }
|
||||
|
||||
public string[] AdminRoles { get; set; }
|
||||
|
||||
public string[] Roles { get; set; }
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>SSO</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-role="page" class="page type-interior pluginConfigurationPage esqConfigurationPage">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<form class="esqConfigurationForm">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">SSO Settings:</h2>
|
||||
<a is="emby-button" class="raised button-alt headerHelpButton" target="_blank" href="https://github.com/9p4/jellyfin-plugin-sso">${Help}</a>
|
||||
</div>
|
||||
<div class="verticalSection" is="emby-collapse" title="SSO Server Settings">
|
||||
<button id="newSaml" is="emby-button">
|
||||
<span>Add new SAML provider</span>
|
||||
</button>
|
||||
<button id="newOID" is="emby-button">
|
||||
<span>Add new OpenID provider</span>
|
||||
</button>
|
||||
<div class="collapseContent" id="default">
|
||||
<div class="samlProviderWrapper">
|
||||
<div class="samlProvider">
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtSamlEndpoint" required placeholder="https://saml-provider.example.com/login" label="SAML Endpoint" />
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtSamlClientId" required placeholder="myjellyfin.example.com" label="SAML Client ID" />
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<textarea is="emby-textarea" id="txtSamlCertificate" placeholder="Base64-encoded realm certificate" label="SAML Certificate"></textarea>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSamlEnable" />
|
||||
<span>Enable Provider</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="btnSaveSettings" is="emby-button" type="submit" value="submit" class="raised button block">
|
||||
<span>Update Provider</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
var SSOConfigurationPage = {
|
||||
pluginUniqueId: "505ce9d1-d916-42fa-86ca-673ef241d7df",
|
||||
};
|
||||
|
||||
document.querySelector('.esqConfigurationPage').addEventListener("pageshow", function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
window.ApiClient.getPluginConfiguration(SSOConfigurationPage.pluginUniqueId).then(function (config) {
|
||||
SSOConfigurationPage.txtSamlEndpoint.value = config.SamlEndpoint;
|
||||
SSOConfigurationPage.txtSamlCertificate.value = config.SamlCertificate;
|
||||
SSOConfigurationPage.txtSamlClientId.value = config.SamlClientId;
|
||||
});
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
|
||||
var form = document.querySelector(".esqConfigurationForm");
|
||||
form.addEventListener("submit", function(e){
|
||||
e.preventDefault();
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
window.ApiClient.getPluginConfiguration(SSOConfigurationPage.pluginUniqueId).then(function (config) {
|
||||
config.SamlCertificate = SSOConfigurationPage.txtSamlCertificate.value;
|
||||
config.SamlEndpoint = SSOConfigurationPage.txtSamlEndpoint.value;
|
||||
config.SamlClientId = SSOConfigurationPage.txtSamlClientId.value;
|
||||
window.ApiClient.updatePluginConfiguration(SSOConfigurationPage.pluginUniqueId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
// Disable default form submission
|
||||
return false;
|
||||
});
|
||||
|
||||
var newSaml = document.getElementById("newSaml");
|
||||
newSaml.addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
Dashboard.showLoadingMsg();
|
||||
fetch(window.ApiClient.getUrl("sso/SAML/Get?api_key=" + window.ApiClient.accessToken()))
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error " + response.status);
|
||||
}
|
||||
return response.json();
|
||||
}).then(json => {console.log(json)});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,36 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.SSO_Auth</RootNamespace>
|
||||
<AssemblyVersion>2.0.1.0</AssemblyVersion>
|
||||
<FileVersion>2.0.1.0</FileVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Config\configPage.html" />
|
||||
<EmbeddedResource Include="Config\configPage.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Plugin.SSO_Auth.Config;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SSO_Auth;
|
||||
|
||||
public class SSOPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
public SSOPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public static SSOPlugin Instance { get; private set; }
|
||||
|
||||
public override string Name => "SSO-Auth";
|
||||
|
||||
public override Guid Id => Guid.Parse("505ce9d1-d916-42fa-86ca-673ef241d7df");
|
||||
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
yield return new PluginPageInfo
|
||||
{
|
||||
Name = Name,
|
||||
EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
/*
|
||||
Was Jitbit's simple SAML 2.0 component for ASP.NET
|
||||
https://github.com/jitbit/AspNetSaml/
|
||||
(c) Jitbit LP, 2016
|
||||
Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/)
|
||||
version 1.2.3
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using System.Xml;
|
||||
|
||||
namespace Jellyfin.Plugin.SSO_Auth;
|
||||
|
||||
public class Response
|
||||
{
|
||||
private readonly X509Certificate2 _certificate;
|
||||
private XmlDocument _xmlDoc;
|
||||
private XmlNamespaceManager _xmlNameSpaceManager; // we need this one to run our XPath queries on the SAML XML
|
||||
|
||||
public Response(string certificateStr, string responseString)
|
||||
: this(Convert.FromBase64String(certificateStr), responseString)
|
||||
{
|
||||
}
|
||||
|
||||
public Response(byte[] certificateBytes, string responseString) : this(certificateBytes)
|
||||
{
|
||||
LoadXmlFromBase64(responseString);
|
||||
}
|
||||
|
||||
public Response(string certificateStr) : this(Convert.FromBase64String(certificateStr))
|
||||
{
|
||||
}
|
||||
|
||||
public Response(byte[] certificateBytes)
|
||||
{
|
||||
_certificate = new X509Certificate2(certificateBytes);
|
||||
}
|
||||
|
||||
public string Xml => _xmlDoc.OuterXml;
|
||||
|
||||
public void LoadXml(string xml)
|
||||
{
|
||||
_xmlDoc = new XmlDocument();
|
||||
_xmlDoc.PreserveWhitespace = true;
|
||||
_xmlDoc.XmlResolver = null;
|
||||
_xmlDoc.LoadXml(xml);
|
||||
|
||||
_xmlNameSpaceManager = GetNamespaceManager(); // lets construct a "manager" for XPath queries
|
||||
}
|
||||
|
||||
public void LoadXmlFromBase64(string response)
|
||||
{
|
||||
LoadXml(Encoding.UTF8.GetString(Convert.FromBase64String(response)));
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
var nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager);
|
||||
|
||||
var signedXml = new SignedXml(_xmlDoc);
|
||||
|
||||
if (nodeList.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
signedXml.LoadXml((XmlElement)nodeList[0]);
|
||||
return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired();
|
||||
}
|
||||
|
||||
// an XML signature can "cover" not the whole document, but only a part of it
|
||||
// .NET's built in "CheckSignature" does not cover this case, it will validate to true.
|
||||
// We should check the signature reference, so it "references" the id of the root document element! If not - it's a hack
|
||||
private bool ValidateSignatureReference(SignedXml signedXml)
|
||||
{
|
||||
if (signedXml.SignedInfo.References.Count != 1) // no ref at all
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var reference = (Reference)signedXml.SignedInfo.References[0];
|
||||
var id = reference.Uri.Substring(1);
|
||||
|
||||
var idElement = signedXml.GetIdElement(_xmlDoc, id);
|
||||
|
||||
if (idElement == _xmlDoc.DocumentElement)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else // sometimes its not the "root" doc-element that is being signed, but the "assertion" element
|
||||
{
|
||||
var assertionNode = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement;
|
||||
if (assertionNode != idElement)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsExpired()
|
||||
{
|
||||
var expirationDate = DateTime.MaxValue;
|
||||
var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager);
|
||||
if (node != null && node.Attributes["NotOnOrAfter"] != null)
|
||||
{
|
||||
DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate);
|
||||
}
|
||||
|
||||
return DateTime.UtcNow > expirationDate.ToUniversalTime();
|
||||
}
|
||||
|
||||
public string GetNameID()
|
||||
{
|
||||
var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager);
|
||||
return node.InnerText;
|
||||
}
|
||||
|
||||
public virtual string GetUpn()
|
||||
{
|
||||
return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn");
|
||||
}
|
||||
|
||||
public virtual string GetEmail()
|
||||
{
|
||||
return GetCustomAttribute("User.email")
|
||||
// some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
|
||||
?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
|
||||
// some providers put last name into an attribute named "mail"
|
||||
?? GetCustomAttribute("mail");
|
||||
}
|
||||
|
||||
public virtual string GetFirstName()
|
||||
{
|
||||
return GetCustomAttribute("first_name")
|
||||
// some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
|
||||
?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")
|
||||
?? GetCustomAttribute("User.FirstName")
|
||||
// some providers put last name into an attribute named "givenName"
|
||||
?? GetCustomAttribute("givenName");
|
||||
}
|
||||
|
||||
public virtual string GetLastName()
|
||||
{
|
||||
return GetCustomAttribute("last_name")
|
||||
// some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
|
||||
?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname")
|
||||
?? GetCustomAttribute("User.LastName")
|
||||
// some providers put last name into an attribute named "sn"
|
||||
?? GetCustomAttribute("sn");
|
||||
}
|
||||
|
||||
public virtual string GetDepartment()
|
||||
{
|
||||
return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department")
|
||||
?? GetCustomAttribute("department");
|
||||
}
|
||||
|
||||
public virtual string GetPhone()
|
||||
{
|
||||
return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone")
|
||||
?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber");
|
||||
}
|
||||
|
||||
public virtual string GetCompany()
|
||||
{
|
||||
return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname")
|
||||
?? GetCustomAttribute("organization")
|
||||
?? GetCustomAttribute("User.CompanyName");
|
||||
}
|
||||
|
||||
public virtual string GetLocation()
|
||||
{
|
||||
return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location")
|
||||
?? GetCustomAttribute("physicalDeliveryOfficeName");
|
||||
}
|
||||
|
||||
public string GetCustomAttribute(string attr)
|
||||
{
|
||||
var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
|
||||
return node?.InnerText;
|
||||
}
|
||||
|
||||
public List<string> GetCustomAttributes(string attr)
|
||||
{
|
||||
var node = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
|
||||
List<string> output = new List<string>();
|
||||
foreach (XmlNode item in node)
|
||||
{
|
||||
output.Add(item?.InnerText);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces
|
||||
// see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary
|
||||
private XmlNamespaceManager GetNamespaceManager()
|
||||
{
|
||||
var manager = new XmlNamespaceManager(_xmlDoc.NameTable);
|
||||
manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
|
||||
manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
|
||||
manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
|
||||
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthRequest
|
||||
{
|
||||
private readonly string _id;
|
||||
private readonly string _issueInstant;
|
||||
|
||||
private readonly string _issuer;
|
||||
private readonly string _assertionConsumerServiceUrl;
|
||||
|
||||
public AuthRequest(string issuer, string assertionConsumerServiceUrl)
|
||||
{
|
||||
_id = "_" + Guid.NewGuid().ToString();
|
||||
_issueInstant = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
_issuer = issuer;
|
||||
_assertionConsumerServiceUrl = assertionConsumerServiceUrl;
|
||||
}
|
||||
|
||||
public enum AuthRequestFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64 request.
|
||||
/// </summary>
|
||||
Base64 = 1
|
||||
}
|
||||
|
||||
public string GetRequest(AuthRequestFormat format)
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
var xws = new XmlWriterSettings();
|
||||
xws.OmitXmlDeclaration = true;
|
||||
|
||||
using (var xw = XmlWriter.Create(sw, xws))
|
||||
{
|
||||
xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol");
|
||||
xw.WriteAttributeString("ID", _id);
|
||||
xw.WriteAttributeString("Version", "2.0");
|
||||
xw.WriteAttributeString("IssueInstant", _issueInstant);
|
||||
xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
|
||||
xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl);
|
||||
|
||||
xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion");
|
||||
xw.WriteString(_issuer);
|
||||
xw.WriteEndElement();
|
||||
|
||||
xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol");
|
||||
xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");
|
||||
xw.WriteAttributeString("AllowCreate", "true");
|
||||
xw.WriteEndElement();
|
||||
|
||||
/*
|
||||
xw.WriteStartElement("samlp", "RequestedAuthnContext", "urn:oasis:names:tc:SAML:2.0:protocol");
|
||||
xw.WriteAttributeString("Comparison", "exact");
|
||||
xw.WriteStartElement("saml", "AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:assertion");
|
||||
xw.WriteString("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteEndElement();
|
||||
*/
|
||||
|
||||
xw.WriteEndElement();
|
||||
}
|
||||
|
||||
if (format == AuthRequestFormat.Base64)
|
||||
{
|
||||
// byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(sw.ToString());
|
||||
// return System.Convert.ToBase64String(toEncodeAsBytes);
|
||||
|
||||
// https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E
|
||||
var memoryStream = new MemoryStream();
|
||||
var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false));
|
||||
writer.Write(sw.ToString());
|
||||
writer.Close();
|
||||
var result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring.
|
||||
/// </summary>
|
||||
/// <param name="samlEndpoint">The SAML endpoint.</param>
|
||||
/// <param name="relayState">The relay state.</param>
|
||||
/// <returns>The redirect url.</returns>
|
||||
public string GetRedirectUrl(string samlEndpoint, string relayState = null)
|
||||
{
|
||||
var queryStringSeparator = samlEndpoint.Contains('?') ? "&" : "?";
|
||||
|
||||
var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + HttpUtility.UrlEncode(GetRequest(AuthRequestFormat.Base64));
|
||||
|
||||
if (!string.IsNullOrEmpty(relayState))
|
||||
{
|
||||
url += "&RelayState=" + HttpUtility.UrlEncode(relayState);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
namespace Jellyfin.Plugin.SSO_Auth;
|
||||
|
||||
public static class WebResponse
|
||||
{
|
||||
public static readonly string Base = @"<!DOCTYPE html>
|
||||
<html><head></head><body><script>
|
||||
function isTv() {
|
||||
// This is going to be really difficult to get right
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
// The OculusBrowsers userAgent also has the samsungbrowser defined but is not a tv.
|
||||
if (userAgent.indexOf('oculusbrowser') !== -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userAgent.indexOf('tv') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userAgent.indexOf('samsungbrowser') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userAgent.indexOf('viera') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isWeb0s()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isWeb0s() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
if (userAgent.indexOf('netcast') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userAgent.indexOf('web0s') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMobile(userAgent) {
|
||||
const terms = [
|
||||
'mobi',
|
||||
'ipad',
|
||||
'iphone',
|
||||
'ipod',
|
||||
'silk',
|
||||
'gt-p1000',
|
||||
'nexus 7',
|
||||
'kindle fire',
|
||||
'opera mini'
|
||||
];
|
||||
|
||||
const lower = userAgent.toLowerCase();
|
||||
|
||||
for (let i = 0, length = terms.length; i < length; i++) {
|
||||
if (lower.indexOf(terms[i]) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasKeyboard(browser) {
|
||||
if (browser.touch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (browser.xboxOne) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (browser.ps4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (browser.edgeUwp) {
|
||||
// This is OK for now, but this won't always be true
|
||||
// Should we use this?
|
||||
// https://gist.github.com/wagonli/40d8a31bd0d6f0dd7a5d
|
||||
return true;
|
||||
}
|
||||
|
||||
if (browser.tv) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function iOSversion() {
|
||||
// MacIntel: Apple iPad Pro 11 iOS 13.1
|
||||
if (/iP(hone|od|ad)|MacIntel/.test(navigator.platform)) {
|
||||
const tests = [
|
||||
// Original test for getting full iOS version number in iOS 2.0+
|
||||
/OS (\d+)_(\d+)_?(\d+)?/,
|
||||
// Test for iPads running iOS 13+ that can only get the major OS version
|
||||
/Version\/(\d+)/
|
||||
];
|
||||
for (const test of tests) {
|
||||
const matches = (navigator.appVersion).match(test);
|
||||
if (matches) {
|
||||
return [
|
||||
parseInt(matches[1], 10),
|
||||
parseInt(matches[2] || 0, 10),
|
||||
parseInt(matches[3] || 0, 10)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function web0sVersion(browser) {
|
||||
// Detect webOS version by web engine version
|
||||
|
||||
if (browser.chrome) {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
if (userAgent.indexOf('netcast') !== -1) {
|
||||
// The built-in browser (NetCast) may have a version that doesn't correspond to the actual web engine
|
||||
// Since there is no reliable way to detect webOS version, we return an undefined version
|
||||
|
||||
console.warn('Unable to detect webOS version - NetCast');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// The next is only valid for the app
|
||||
|
||||
if (browser.versionMajor >= 79) {
|
||||
return 6;
|
||||
} else if (browser.versionMajor >= 68) {
|
||||
return 5;
|
||||
} else if (browser.versionMajor >= 53) {
|
||||
return 4;
|
||||
} else if (browser.versionMajor >= 38) {
|
||||
return 3;
|
||||
} else if (browser.versionMajor >= 34) {
|
||||
// webOS 2 browser
|
||||
return 2;
|
||||
} else if (browser.versionMajor >= 26) {
|
||||
// webOS 1 browser
|
||||
return 1;
|
||||
}
|
||||
} else if (browser.versionMajor >= 538) {
|
||||
// webOS 2 app
|
||||
return 2;
|
||||
} else if (browser.versionMajor >= 537) {
|
||||
// webOS 1 app
|
||||
return 1;
|
||||
}
|
||||
|
||||
console.error('Unable to detect webOS version');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let _supportsCssAnimation;
|
||||
let _supportsCssAnimationWithPrefix;
|
||||
function supportsCssAnimation(allowPrefix) {
|
||||
// TODO: Assess if this is still needed, as all of our targets should natively support CSS animations.
|
||||
if (allowPrefix) {
|
||||
if (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false) {
|
||||
return _supportsCssAnimationWithPrefix;
|
||||
}
|
||||
} else {
|
||||
if (_supportsCssAnimation === true || _supportsCssAnimation === false) {
|
||||
return _supportsCssAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
let animation = false;
|
||||
const domPrefixes = ['Webkit', 'O', 'Moz'];
|
||||
const elm = document.createElement('div');
|
||||
|
||||
if (elm.style.animationName !== undefined) {
|
||||
animation = true;
|
||||
}
|
||||
|
||||
if (animation === false && allowPrefix) {
|
||||
for (let i = 0; i < domPrefixes.length; i++) {
|
||||
if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
|
||||
animation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allowPrefix) {
|
||||
_supportsCssAnimationWithPrefix = animation;
|
||||
return _supportsCssAnimationWithPrefix;
|
||||
} else {
|
||||
_supportsCssAnimation = animation;
|
||||
return _supportsCssAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
const uaMatch = function (ua) {
|
||||
ua = ua.toLowerCase();
|
||||
|
||||
const match = /(edg)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(edga)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(edgios)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(edge)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(opera)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(opr)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(chrome)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(safari)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(firefox)[ /]([\w.]+)/.exec(ua) ||
|
||||
ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
|
||||
[];
|
||||
|
||||
const versionMatch = /(version)[ /]([\w.]+)/.exec(ua);
|
||||
|
||||
let platform_match = /(ipad)/.exec(ua) ||
|
||||
/(iphone)/.exec(ua) ||
|
||||
/(windows)/.exec(ua) ||
|
||||
/(android)/.exec(ua) ||
|
||||
[];
|
||||
|
||||
let browser = match[1] || '';
|
||||
|
||||
if (browser === 'edge') {
|
||||
platform_match = [''];
|
||||
}
|
||||
|
||||
if (browser === 'opr') {
|
||||
browser = 'opera';
|
||||
}
|
||||
|
||||
let version;
|
||||
if (versionMatch && versionMatch.length > 2) {
|
||||
version = versionMatch[2];
|
||||
}
|
||||
|
||||
version = version || match[2] || '0';
|
||||
|
||||
let versionMajor = parseInt(version.split('.')[0]);
|
||||
|
||||
if (isNaN(versionMajor)) {
|
||||
versionMajor = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
browser: browser,
|
||||
version: version,
|
||||
platform: platform_match[0] || '',
|
||||
versionMajor: versionMajor
|
||||
};
|
||||
};
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
const matched = uaMatch(userAgent);
|
||||
const browser = {};
|
||||
|
||||
if (matched.browser) {
|
||||
browser[matched.browser] = true;
|
||||
browser.version = matched.version;
|
||||
browser.versionMajor = matched.versionMajor;
|
||||
}
|
||||
|
||||
if (matched.platform) {
|
||||
browser[matched.platform] = true;
|
||||
}
|
||||
|
||||
browser.edgeChromium = browser.edg || browser.edga || browser.edgios;
|
||||
|
||||
if (!browser.chrome && !browser.edgeChromium && !browser.edge && !browser.opera && userAgent.toLowerCase().indexOf('webkit') !== -1) {
|
||||
browser.safari = true;
|
||||
}
|
||||
|
||||
browser.osx = userAgent.toLowerCase().indexOf('mac os x') !== -1;
|
||||
|
||||
// This is a workaround to detect iPads on iOS 13+ that report as desktop Safari
|
||||
// This may break in the future if Apple releases a touchscreen Mac
|
||||
// https://forums.developer.apple.com/thread/119186
|
||||
if (browser.osx && !browser.iphone && !browser.ipod && !browser.ipad && navigator.maxTouchPoints > 1) {
|
||||
browser.ipad = true;
|
||||
}
|
||||
|
||||
if (userAgent.toLowerCase().indexOf('playstation 4') !== -1) {
|
||||
browser.ps4 = true;
|
||||
browser.tv = true;
|
||||
}
|
||||
|
||||
if (isMobile(userAgent)) {
|
||||
browser.mobile = true;
|
||||
}
|
||||
|
||||
if (userAgent.toLowerCase().indexOf('xbox') !== -1) {
|
||||
browser.xboxOne = true;
|
||||
browser.tv = true;
|
||||
}
|
||||
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
|
||||
browser.tizen = userAgent.toLowerCase().indexOf('tizen') !== -1 || window.tizen != null;
|
||||
browser.web0s = isWeb0s();
|
||||
browser.edgeUwp = browser.edge && (userAgent.toLowerCase().indexOf('msapphost') !== -1 || userAgent.toLowerCase().indexOf('webview') !== -1);
|
||||
|
||||
if (browser.web0s) {
|
||||
browser.web0sVersion = web0sVersion(browser);
|
||||
} else if (browser.tizen) {
|
||||
// UserAgent string contains 'Safari' and 'safari' is set by matched browser, but we only want 'tizen' to be true
|
||||
delete browser.safari;
|
||||
|
||||
const v = (navigator.appVersion).match(/Tizen (\d+).(\d+)/);
|
||||
browser.tizenVersion = parseInt(v[1]);
|
||||
} else {
|
||||
browser.orsay = userAgent.toLowerCase().indexOf('smarthub') !== -1;
|
||||
}
|
||||
|
||||
if (browser.edgeUwp) {
|
||||
browser.edge = true;
|
||||
}
|
||||
|
||||
browser.tv = isTv();
|
||||
browser.operaTv = browser.tv && userAgent.toLowerCase().indexOf('opr/') !== -1;
|
||||
|
||||
if (browser.mobile || browser.tv) {
|
||||
browser.slow = true;
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
if (('ontouchstart' in window) || (navigator.maxTouchPoints > 0)) {
|
||||
browser.touch = true;
|
||||
}
|
||||
}
|
||||
|
||||
browser.keyboard = hasKeyboard(browser);
|
||||
browser.supportsCssAnimation = supportsCssAnimation;
|
||||
|
||||
browser.iOS = browser.ipad || browser.iphone || browser.ipod;
|
||||
|
||||
if (browser.iOS) {
|
||||
browser.iOSVersion = iOSversion();
|
||||
|
||||
if (browser.iOSVersion && browser.iOSVersion.length >= 2) {
|
||||
browser.iOSVersion = browser.iOSVersion[0] + (browser.iOSVersion[1] / 10);
|
||||
}
|
||||
}
|
||||
|
||||
function getDeviceName() {
|
||||
var deviceName = '';
|
||||
if (browser.tizen) {
|
||||
deviceName = 'Samsung Smart TV';
|
||||
} else if (browser.web0s) {
|
||||
deviceName = 'LG Smart TV';
|
||||
} else if (browser.operaTv) {
|
||||
deviceName = 'Opera TV';
|
||||
} else if (browser.xboxOne) {
|
||||
deviceName = 'Xbox One';
|
||||
} else if (browser.ps4) {
|
||||
deviceName = 'Sony PS4';
|
||||
} else if (browser.chrome) {
|
||||
deviceName = 'Chrome';
|
||||
} else if (browser.edgeChromium) {
|
||||
deviceName = 'Edge Chromium';
|
||||
} else if (browser.edge) {
|
||||
deviceName = 'Edge';
|
||||
} else if (browser.firefox) {
|
||||
deviceName = 'Firefox';
|
||||
} else if (browser.opera) {
|
||||
deviceName = 'Opera';
|
||||
} else if (browser.safari) {
|
||||
deviceName = 'Safari';
|
||||
} else {
|
||||
deviceName = 'Web Browser';
|
||||
}
|
||||
|
||||
if (browser.ipad) {
|
||||
deviceName += ' iPad';
|
||||
} else if (browser.iphone) {
|
||||
deviceName += ' iPhone';
|
||||
} else if (browser.android) {
|
||||
deviceName += ' Android';
|
||||
}
|
||||
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
";
|
||||
|
||||
public static string OIDGenerator(string data, string provider, string baseUrl)
|
||||
{
|
||||
return Base + @"
|
||||
async function main() {
|
||||
var data = '" + data + @"';
|
||||
var deviceId = localStorage.getItem(""_deviceId2"");
|
||||
var appName = ""Jellyfin Web"";
|
||||
var appVersion = ""10.8.0"";
|
||||
var deviceName = getDeviceName();
|
||||
var provider = '" + provider + @"';
|
||||
|
||||
var request = {'deviceID': deviceId, 'appName': appName, 'appVersion': appVersion, deviceName: 'deviceName', data: data, provider: '" + provider + @"'};
|
||||
|
||||
var url = '" + baseUrl + @"/sso/OID/Auth';
|
||||
|
||||
let response = await new Promise(resolve => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = function(e) {
|
||||
resolve(xhr.response);
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
resolve(undefined);
|
||||
};
|
||||
xhr.send(JSON.stringify(request));
|
||||
})
|
||||
var responseJson = JSON.parse(response);
|
||||
var userId = 'user-' + responseJson['User']['Id'] + '-' + responseJson['User']['ServerId'];
|
||||
responseJson['User']['EnableAutoLogin'] = true;
|
||||
localStorage.setItem(userId, JSON.stringify(responseJson['User']));
|
||||
var jfCreds = JSON.parse(localStorage.getItem('jellyfin_credentials'));
|
||||
jfCreds['Servers'][0]['AccessToken'] = responseJson['AccessToken'];
|
||||
jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
|
||||
localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
|
||||
localStorage.setItem('enableAutoLogin', 'true');
|
||||
window.location.replace('/');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
main();
|
||||
});
|
||||
|
||||
</script></body></html>";
|
||||
}
|
||||
|
||||
public static string SamlGenerator(string xml, string provider, string baseUrl)
|
||||
{
|
||||
return Base + @"
|
||||
async function main() {
|
||||
var xml = '" + xml + @"';
|
||||
var deviceId = localStorage.getItem(""_deviceId2"");
|
||||
var appName = ""Jellyfin Web"";
|
||||
var appVersion = ""10.8.0"";
|
||||
var deviceName = getDeviceName();
|
||||
var provider = '" + provider + @"';
|
||||
|
||||
var request = {'deviceID': deviceId, 'appName': appName, 'appVersion': appVersion, deviceName: 'deviceName', data: xml, provider: '" + provider + @"'};
|
||||
|
||||
var url = '" + baseUrl + @"/sso/SAML/Auth';
|
||||
|
||||
let response = await new Promise(resolve => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = function(e) {
|
||||
resolve(xhr.response);
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
resolve(undefined);
|
||||
};
|
||||
xhr.send(JSON.stringify(request));
|
||||
})
|
||||
var responseJson = JSON.parse(response);
|
||||
var userId = 'user-' + responseJson['User']['Id'] + '-' + responseJson['User']['ServerId'];
|
||||
responseJson['User']['EnableAutoLogin'] = true;
|
||||
localStorage.setItem(userId, JSON.stringify(responseJson['User']));
|
||||
var jfCreds = JSON.parse(localStorage.getItem('jellyfin_credentials'));
|
||||
jfCreds['Servers'][0]['AccessToken'] = responseJson['AccessToken'];
|
||||
jfCreds['Servers'][0]['UserId'] = responseJson['User']['Id'];
|
||||
localStorage.setItem('jellyfin_credentials', JSON.stringify(jfCreds));
|
||||
localStorage.setItem('enableAutoLogin', 'true');
|
||||
window.location.replace('/');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
main();
|
||||
});
|
||||
|
||||
</script></body></html>";
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
name: "SSO Authentication"
|
||||
guid: "505ce9d1-d916-42fa-86ca-673ef241d7df"
|
||||
imageUrl: "https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/main/img/logo.png"
|
||||
version: "2.0.1.0"
|
||||
targetAbi: "10.8.0.0"
|
||||
framework: "net6.0"
|
||||
owner: "9p4"
|
||||
overview: "Authenticate users against an SSO provider."
|
||||
description: |
|
||||
This plugin allows users to sign in through an SSO provider (such as Google, Facebook, or your own provider). This enables one-click signin.
|
||||
Review documentation at https://github.com/9p4/jellyfin-plugin-sso
|
||||
category: "Authentication"
|
||||
artifacts:
|
||||
- "SSO-Auth.dll"
|
||||
- "IdentityModel.OidcClient.dll"
|
||||
- "IdentityModel.dll"
|
||||
changelog: |
|
||||
2.0.1.0: Fix improper artifact loading
|
||||
2.0.0.0: Add RBAC and Google support
|
||||
1.0.0.0: Initial Release
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
@@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
|
||||
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
|
||||
<Rule Id="SA1202" Action="Info" />
|
||||
<!-- disable warning SA1204: Static members must appear before non-static members -->
|
||||
<Rule Id="SA1204" Action="Info" />
|
||||
<!-- disable warning SA1404: Code analysis suppression should have justification -->
|
||||
<Rule Id="SA1404" Action="Info" />
|
||||
|
||||
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
|
||||
<Rule Id="SA1009" Action="None" />
|
||||
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
|
||||
<Rule Id="SA1101" Action="None" />
|
||||
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
|
||||
<Rule Id="SA1108" Action="None" />
|
||||
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
|
||||
<Rule Id="SA1128" Action="None" />
|
||||
<!-- disable warning SA1130: Use lambda syntax -->
|
||||
<Rule Id="SA1130" Action="None" />
|
||||
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
|
||||
<Rule Id="SA1200" Action="None" />
|
||||
<!-- disable warning SA1309: Fields must not begin with an underscore -->
|
||||
<Rule Id="SA1309" Action="None" />
|
||||
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
|
||||
<Rule Id="SA1413" Action="None" />
|
||||
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
|
||||
<Rule Id="SA1512" Action="None" />
|
||||
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
|
||||
<Rule Id="SA1515" Action="None" />
|
||||
<!-- disable warning SA1600: Elements should be documented -->
|
||||
<Rule Id="SA1600" Action="None" />
|
||||
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
|
||||
<Rule Id="SA1633" Action="None" />
|
||||
</Rules>
|
||||
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
|
||||
<!-- disable warning CA1031: Do not catch general exception types -->
|
||||
<Rule Id="CA1031" Action="Info" />
|
||||
<!-- disable warning CA1032: Implement standard exception constructors -->
|
||||
<Rule Id="CA1032" Action="Info" />
|
||||
<!-- disable warning CA1062: Validate arguments of public methods -->
|
||||
<Rule Id="CA1062" Action="Info" />
|
||||
<!-- disable warning CA1716: Identifiers should not match keywords -->
|
||||
<Rule Id="CA1716" Action="Info" />
|
||||
<!-- disable warning CA1720: Identifiers should not contain type names -->
|
||||
<Rule Id="CA1720" Action="Info" />
|
||||
<!-- disable warning CA1812: internal class that is apparently never instantiated.
|
||||
If so, remove the code from the assembly.
|
||||
If this class is intended to contain only static members, make it static -->
|
||||
<Rule Id="CA1812" Action="Info" />
|
||||
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
|
||||
<Rule Id="CA1822" Action="Info" />
|
||||
<!-- disable warning CA2000: Dispose objects before losing scope -->
|
||||
<Rule Id="CA2000" Action="Info" />
|
||||
|
||||
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
|
||||
<Rule Id="CA1054" Action="None" />
|
||||
<!-- disable warning CA1055: URI return values should not be strings -->
|
||||
<Rule Id="CA1055" Action="None" />
|
||||
<!-- disable warning CA1056: URI properties should not be strings -->
|
||||
<Rule Id="CA1056" Action="None" />
|
||||
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
|
||||
<Rule Id="CA1303" Action="None" />
|
||||
<!-- disable warning CA1308: Normalize strings to uppercase -->
|
||||
<Rule Id="CA1308" Action="None" />
|
||||
</Rules>
|
||||
</RuleSet>
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
[
|
||||
{
|
||||
"category": "Authentication",
|
||||
"description": "This plugin allows users to sign in through an SSO provider (such as Google, Facebook, or your own provider). This enables one-click signin.\nReview documentation at https://github.com/9p4/jellyfin-plugin-sso\n",
|
||||
"guid": "505ce9d1-d916-42fa-86ca-673ef241d7df",
|
||||
"name": "SSO Authentication",
|
||||
"overview": "Authenticate users against an SSO provider.",
|
||||
"owner": "9p4",
|
||||
"versions": [
|
||||
{
|
||||
"changelog": "There is a critical security fix for SAML. Please update as soon as possible. Thanks to N.P. for finding the issue.\r\n\r\nAdditional bugfixes are also included.\r\n\r\n## What's Changed\r\n* Add Pocket ID config steps by @mhlas7 in https://github.com/9p4/jellyfin-plugin-sso/pull/312\r\n* fix: invalidate state immediately after auth by @jon4hz in https://github.com/9p4/jellyfin-plugin-sso/pull/343\r\n* Add kanidm config steps by @Joker9944 in https://github.com/9p4/jellyfin-plugin-sso/pull/347\r\n* Update README dead link, add warning for permission overwritten by @ForsakenRei in https://github.com/9p4/jellyfin-plugin-sso/pull/318\r\n* Bugfix: Prevent KeyNotFoundExceptions in OidPost by @mandos21 in https://github.com/9p4/jellyfin-plugin-sso/pull/349\r\n\r\n## New Contributors\r\n* @mhlas7 made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/312\r\n* @jon4hz made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/343\r\n* @Joker9944 made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/347\r\n* @ForsakenRei made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/318\r\n* @mandos21 made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/349\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v4.0.0.3...v4.0.0.4",
|
||||
"checksum": "1e7908585ebee256203a0869dcdfbaf8",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v4.0.0.4/sso-authentication_4.0.0.4.zip",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"timestamp": "2026-03-20T03:13:34Z",
|
||||
"version": "4.0.0.4"
|
||||
},
|
||||
{
|
||||
"changelog": "More CI fixes\r\n\r\n## What's Changed\r\n* fix: consistently use OpenID instead of OID on the config page by @CFenner in https://github.com/9p4/jellyfin-plugin-sso/pull/193\r\n* Support DisableHttps/DoNotValidateIssuerName in SSOController.OidChallenge by @andreblanke in https://github.com/9p4/jellyfin-plugin-sso/pull/239\r\n* Allow to disable pushed authorization by @bdovaz in https://github.com/9p4/jellyfin-plugin-sso/pull/244\r\n* Update oddstr13/jellyfin-plugin-repository-manager by @bdovaz in https://github.com/9p4/jellyfin-plugin-sso/pull/247\r\n* Avatar via OIDC Provider by @Estyms in https://github.com/9p4/jellyfin-plugin-sso/pull/202\r\n* Allow port override by @bdovaz in https://github.com/9p4/jellyfin-plugin-sso/pull/246\r\n* Fix artifact names by @bdovaz in https://github.com/9p4/jellyfin-plugin-sso/pull/248\r\n* Prevent force updating username by @FeikoJoosten in https://github.com/9p4/jellyfin-plugin-sso/pull/251\r\n* remove deprecated redirect uri from readme by @hendrik1120 in https://github.com/9p4/jellyfin-plugin-sso/pull/280\r\n* Match the styling of the redirection page with Jellyfin colors and font by @kernelb00t in https://github.com/9p4/jellyfin-plugin-sso/pull/269\r\n* Use punycode URL for redirect page - OIDC by @babbitt in https://github.com/9p4/jellyfin-plugin-sso/pull/265\r\n\r\n## New Contributors\r\n* @CFenner made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/193\r\n* @andreblanke made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/239\r\n* @bdovaz made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/244\r\n* @Estyms made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/202\r\n* @FeikoJoosten made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/251\r\n* @kernelb00t made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/269\r\n* @babbitt made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/265\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.5.2.4...v4.0.0.3",
|
||||
"checksum": "ea6673c5c9aeddcf01a38604046e7ffc",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v4.0.0.3/sso-authentication_4.0.0.3.zip",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"timestamp": "2025-10-21T14:39:25Z",
|
||||
"version": "4.0.0.3"
|
||||
},
|
||||
{
|
||||
"changelog": "## What's Changed\r\n* Update version in SSO-Auth.csproj by @lf- in https://github.com/9p4/jellyfin-plugin-sso/pull/169\r\n* update authelia example to authelia v4.38 by @hendrik1120 in https://github.com/9p4/jellyfin-plugin-sso/pull/170\r\n* Update providers.md to include android app keycloak OIDC redirect URI by @tbelway in https://github.com/9p4/jellyfin-plugin-sso/pull/190\r\n* Update plugin to function with Jellyfin 10.9\r\n\r\n## New Contributors\r\n* @lf- made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/169\r\n* @hendrik1120 made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/170\r\n* @tbelway made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/190\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.5.2.3...v3.5.2.4",
|
||||
"checksum": "e325bcf4c60c5cae3e0cd4614faae07a",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.2.4/sso-authentication_3.5.2.4.zip",
|
||||
"targetAbi": "10.9.0.0",
|
||||
"timestamp": "2024-05-13T13:34:38Z",
|
||||
"version": "3.5.2.4"
|
||||
},
|
||||
{
|
||||
"changelog": "## What's Changed\r\n* Remove jellyfin_credentials before loading iframe by @sbogomolov in https://github.com/9p4/jellyfin-plugin-sso/pull/163\r\n* [allow for better endpoint validation](https://github.com/9p4/jellyfin-plugin-sso/commit/d51e5069d6584953e93f93ec161b909388cd7ebe)\r\n\r\n## New Contributors\r\n* @sbogomolov made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/163\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.5.2.2...v3.5.2.3",
|
||||
"checksum": "ba6ce744fd21c0b9452b23eeeb99e0e1",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.2.3/sso-authentication_3.5.2.3.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2024-02-21T22:11:24Z",
|
||||
"version": "3.5.2.3"
|
||||
},
|
||||
{
|
||||
"changelog": "[allow linking to work with new paths](https://github.com/9p4/jellyfin-plugin-sso/commit/9321c25f2028101cee19f4bf90083aecb60bdba5)\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.5.2.1...v3.5.2.2",
|
||||
"checksum": "94d0f6acf8eaead11c25680ee13cde7f",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.2.2/sso-authentication_3.5.2.2.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2024-01-22T21:39:33Z",
|
||||
"version": "3.5.2.2"
|
||||
},
|
||||
{
|
||||
"changelog": "Hotfix for https://github.com/9p4/jellyfin-plugin-sso/issues/150",
|
||||
"checksum": "a2c47db686895bc7cd4594bdda25286e",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.2.1/sso-authentication_3.5.2.1.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2023-10-18T05:12:18Z",
|
||||
"version": "3.5.2.1"
|
||||
},
|
||||
{
|
||||
"changelog": "feat: allow overriding redirect scheme\r\n\r\nWow, I'm putting out a lot of releases lately. Sorry about that.\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.5.1.1...v3.5.2.0",
|
||||
"checksum": "83b29edbc0c6722606bae9f2de095d88",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.2.0/sso-authentication_3.5.2.0.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2023-08-07T17:17:13Z",
|
||||
"version": "3.5.2.0"
|
||||
},
|
||||
{
|
||||
"changelog": "fix: change iframe URL to point to the web UI instead of the root\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.5.1.0...v3.5.1.1",
|
||||
"checksum": "c5e6dd973ebef1ef80d7d59ef7ad0815",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.1.1/sso-authentication_3.5.1.1.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2023-08-07T12:44:02Z",
|
||||
"version": "3.5.1.1"
|
||||
},
|
||||
{
|
||||
"changelog": "* fix: improve final redirect (#109)\r\n* style: fix configPage style (prettier)\r\n* docs: detail Google requiring disabling of OpenID endpoint validation\r\n* fix: make SAML paths better; improve and add OID discovery defaults\r\n* docs: clarify plugin resetting permissions\r\n* style: fix README.md style\r\n* docs: document how to set login disclaimer (#16)\r\n* feat: clarify redirect paths (#84)\r\n* fix: Remove funding\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.5.0.0...v3.5.1.0",
|
||||
"checksum": "574ab1a090ba1726a01b0861e990eeb8",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.1.0/sso-authentication_3.5.1.0.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2023-08-06T20:38:07Z",
|
||||
"version": "3.5.1.0"
|
||||
},
|
||||
{
|
||||
"changelog": "## What's Changed\r\n* feat: add Live TV management\r\n* fix: make user creation more robust by @fredriklindberg in https://github.com/9p4/jellyfin-plugin-sso/pull/72\r\n* fix: don't include port in redirect URL for 80 and 443 by @fredriklindberg in https://github.com/9p4/jellyfin-plugin-sso/pull/71\r\n* feat: allow configuration of preferred username claim by @9p4 in https://github.com/9p4/jellyfin-plugin-sso/pull/74\r\n* feat: allow for HTTPS to be disabled in OpenID discovery by @9p4 in https://github.com/9p4/jellyfin-plugin-sso/pull/77\r\n* fix: #83 Trim whitespace in user provided strings by @9p4 in https://github.com/9p4/jellyfin-plugin-sso/pull/89\r\n* null check before trim() by @esmondmissen in https://github.com/9p4/jellyfin-plugin-sso/pull/99\r\n* fix: error in logs for #81 by @esmondmissen in https://github.com/9p4/jellyfin-plugin-sso/pull/100\r\n* docs: 📝 add authentik redirect uri step by @adamzvolanek in https://github.com/9p4/jellyfin-plugin-sso/pull/127\r\n* fix: fix null pointer errors in various places\r\n\r\n## New Contributors\r\n* @fredriklindberg made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/72\r\n* @esmondmissen made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/99\r\n* @adamzvolanek made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/127\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.4.0.2...v3.5.0.0",
|
||||
"checksum": "5621a407cce8907bfa5cad9d3ce52924",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.5.0.0/sso-authentication_3.5.0.0.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2023-08-05T22:55:07Z",
|
||||
"version": "3.5.0.0"
|
||||
},
|
||||
{
|
||||
"changelog": "# Self Service: \r\n\r\n* Users can link existing jellyfin accounts to SSO provider accounts\r\n* Users can remove existing links\r\n* Users can configure multiple SSO provider accounts\r\n* Account names no longer have to match across Jellyfin -> SSO provider\r\n * Eg, can link google OIDC, with a `sub` of `asfbeausfahgajsfjasd-ugly-sub` to jellyfin username `matthew`\r\n\r\n## What's Changed\r\n\r\n### Features\r\n\r\n* Implement Canonical Linking + Self Service by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/34\r\n * Account names no longer have to match to be linked\r\n\r\n### Fixes\r\n\r\n* Fix ID Provider incorrectly resetting on login by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/33\r\n\r\n### Documentation\r\n\r\n* Document Provider Specific Configuration, Small Readme Update by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/31\r\n* docs: move build-from-source to contributing section, link vscode config by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/63\r\n* docs: :memo: document new plugin install mechanism by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/56\r\n\r\n### CI / Build\r\n\r\n* Add github action for publishing releases by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/39\r\n* Revert \"Add github action for publishing releases\" by @9p4 in https://github.com/9p4/jellyfin-plugin-sso/pull/40\r\n* Revert \"Revert \"Add github action for publishing releases\"\" by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/46\r\n* pin version of oddstr13/jprm to specific commit by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/48\r\n* ci: pin kevinjil/jprm-action to specific sha by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/50\r\n* ci: ignore prereleases when publishing manifest by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/52\r\n* ci: add checksum to nightly builds by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/53\r\n* ci: only run checksum agaisnt zip files by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/54\r\n* build: ignore .vscode entirely in gitignore by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/62\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.3.0.0...v3.4.0.2",
|
||||
"checksum": "485d67094e39cc5b11470abea6b46427",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.4.0.2/sso-authentication_3.4.0.0.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2022-08-18T09:10:59Z",
|
||||
"version": "3.4.0.0"
|
||||
},
|
||||
{
|
||||
"changelog": "## What's Changed\r\n* Clean up controller by @crobibero in https://github.com/9p4/jellyfin-plugin-sso/pull/12\r\n* Fallback Authentication Provider by @Pfuenzle in https://github.com/9p4/jellyfin-plugin-sso/pull/9\r\n* Add UI Page for OIDC Configuration by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/18\r\n* Add prettier workflow to CI by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/22\r\n* Allow requesting additional scopes in OIDC flows by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/24\r\n* CI: Prettier Workflow: Remove explicit branch ref from checkout by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/25\r\n* CI: lint css files in prettier workflow by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/30\r\n* Improve UX on config page by @matthewstrasiotto in https://github.com/9p4/jellyfin-plugin-sso/pull/27\r\n\r\n## New Contributors\r\n* @matthewstrasiotto made their first contribution in https://github.com/9p4/jellyfin-plugin-sso/pull/18\r\n\r\n**Full Changelog**: https://github.com/9p4/jellyfin-plugin-sso/compare/v3.2.0.0...v3.3.0.0",
|
||||
"checksum": "79357968612a992e6494167fd065dafb",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v3.3.0.0/sso-authentication_3.3.0.0.zip",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"timestamp": "2022-05-22T16:17:07Z",
|
||||
"version": "3.3.0.0"
|
||||
},
|
||||
{
|
||||
"changelog": "Nightly build\n",
|
||||
"checksum": "517d0ece631d5d1ae35dabc8f90b6636",
|
||||
"sourceUrl": "https://github.com/9p4/jellyfin-plugin-sso/releases/download/nightly/sso-authentication_0.0.0.9000.zip",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"timestamp": "2022-07-01T01:20:33Z",
|
||||
"version": "0.0.0.9000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user