sso logout flow

This commit is contained in:
IRHM
2024-12-28 16:01:44 +00:00
parent eb2e2e9f93
commit cbd2cfcfbe
8 changed files with 164 additions and 10 deletions
+13
View File
@@ -31,6 +31,10 @@ type TrustedHeaderAuthSetting struct {
LogoutUrl string `json:"logoutUrl,omitempty"`
}
type TrustedHeaderAuthLogoutDetailsResponse struct {
LogoutUrl string `json:"logoutUrl,omitempty"`
}
// Is trusted header auth configured on this server?
func trustedHeaderAuthIsEnabled() bool {
return Config.HEADER_AUTH.Enabled && Config.HEADER_AUTH.HeaderName != ""
@@ -47,6 +51,15 @@ func setTrustedHeaderAuthSetting(has TrustedHeaderAuthSetting) error {
return nil
}
// Gets proxy logout details.
// Details are accessible to any user for the logout flow.
// If proxy configured should be checked before using this.
func getTrustedHeaderAuthLogoutDetails() *TrustedHeaderAuthLogoutDetailsResponse {
return &TrustedHeaderAuthLogoutDetailsResponse{
LogoutUrl: Config.HEADER_AUTH.LogoutUrl,
}
}
// Login via header sso
func loginTrustedHeaderAuth(user *User, db *gorm.DB) (AuthResponse, error) {
slog.Debug("loginTrustedHeaderAuth: A user is logging in", "username_from_header", user.Username)
+21 -4
View File
@@ -765,14 +765,14 @@ func (b *BaseRouter) addAuthRoutes() {
auth.POST("/proxy", func(c *gin.Context) {
var user User
if !trustedHeaderAuthIsEnabled() {
slog.Error("Request made to login via Proxy, but PROXY_AUTH_HEADER has not been configured.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "Proxy authentication disabled"})
slog.Error("ProxyLogin: SSO has not been configured.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "proxy authentication is disabled"})
return
}
user.Username = c.GetHeader(Config.HEADER_AUTH.HeaderName)
if user.Username == "" {
slog.Error("Request made to login via Proxy, but authentication header was not provided")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "Authentication header missing"})
slog.Error("ProxyLogin: Authentication header is missing.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "authentication header missing"})
return
}
response, err := loginTrustedHeaderAuth(&user, b.db)
@@ -822,6 +822,23 @@ func (b *BaseRouter) addAuthRoutes() {
// IMPORTANT: Routes below here must be authenticated.
auth.Use(AuthRequired(nil))
{
// Request details for logout process for proxy users.
// Any proxy user can request this for logout.
auth.GET("/proxy_logout_details", func(c *gin.Context) {
if !trustedHeaderAuthIsEnabled() {
slog.Error("GetProxy: SSO has not been configured.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "proxy authentication is disabled"})
return
}
userType := c.MustGet("userType").(UserType)
if userType != PROXY_USER {
slog.Error("GetProxy: Non proxy user attempted to fetch proxy logout details.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "you are not a proxy user"})
return
}
c.JSON(http.StatusOK, getTrustedHeaderAuthLogoutDetails())
})
// Request admin token
auth.GET("/admin_token", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
@@ -0,0 +1,90 @@
<script lang="ts">
import { onMount } from "svelte";
import Modal from "../Modal.svelte";
import axios from "axios";
import type { TrustedHeaderAuthLogoutDetailsResponse } from "@/types";
import Spinner from "../Spinner.svelte";
import { clearWatcharrData } from ".";
import { goto } from "$app/navigation";
export let onClose: () => void;
let loadingBtns = true;
let logoutUrl: string | undefined = undefined;
onMount(async () => {
try {
const r = await axios.get<TrustedHeaderAuthLogoutDetailsResponse>(
"/auth/proxy_logout_details"
);
logoutUrl = r?.data?.logoutUrl;
if (!logoutUrl?.toLowerCase()?.startsWith("http")) {
// If no protocol in logoutUrl, set https
logoutUrl = `https://${logoutUrl}`;
}
} catch (err) {
console.error("Failed to get proxy logout details!", err);
}
loadingBtns = false;
});
function logout() {
clearWatcharrData();
goto("/login?noAuto=1");
}
function proxyLogout() {
if (!logoutUrl) {
console.error("proxyLogout: Not supported without a configured logoutUrl.");
return;
}
clearWatcharrData();
window.location.replace(logoutUrl);
}
</script>
<Modal
title="Logout"
desc="You logged in via single sign-on. Did you mean to try logging out?"
{onClose}
>
{#if loadingBtns}
<Spinner />
{:else}
<div>
<p>
{#if logoutUrl}
This is likely what you want: <b
>Fully logout of your single sign-on service and Watcharr</b
>.
{:else}
Single sign-on logout has not been configured on this server. If you are not the server
operator, let them know!
{/if}
</p>
<button disabled={!logoutUrl} on:click={proxyLogout}>Log out of Single Sign-On Service</button
>
<p>
Logging out of Watcharr will clear your local credentials and data, but <b
>you will still be logged in to your single sign-on service</b
>!
</p>
<button on:click={logout}>Log out of Watcharr</button>
</div>
{/if}
</Modal>
<style lang="scss">
div {
display: flex;
flex-flow: column;
p {
margin-bottom: 5px;
}
button:first-of-type {
margin-bottom: 15px;
}
}
</style>
+14
View File
@@ -0,0 +1,14 @@
import { clearAllStores } from "@/store";
/**
* Helper to clear local data in client for logout
* process. Logic is here so it's not duplciated,
* this should help avoid forgetting to copy any
* new logic to other places, which could break stuff.
* Since this is reusable, technically unrelated logic
* should not be included here (eg: redirecting to /login).
*/
export function clearWatcharrData() {
localStorage.removeItem("token");
clearAllStores();
}
+13 -3
View File
@@ -5,6 +5,8 @@
import PageError from "@/lib/PageError.svelte";
import Spinner from "@/lib/Spinner.svelte";
import tooltip from "@/lib/actions/tooltip";
import { clearWatcharrData } from "@/lib/logout";
import ProxyUserLogoutModal from "@/lib/logout/ProxyUserLogoutModal.svelte";
import DetailedMenu from "@/lib/nav/DetailedMenu.svelte";
import FilterMenu from "@/lib/nav/FilterMenu.svelte";
import FollowingMenu from "@/lib/nav/FollowingMenu.svelte";
@@ -25,7 +27,7 @@
userSettings,
watchedList
} from "@/store";
import { UserPermission } from "@/types";
import { UserPermission, UserType } from "@/types";
import axios from "axios";
import { onMount } from "svelte";
@@ -38,6 +40,7 @@
let followingMenuShown = false;
let detailedMenuShown = false;
let tagMenuShown = false;
let proxyUserLogoutShown = false;
$: settings = $userSettings;
$: user = $userInfo;
@@ -91,8 +94,12 @@
}
function logout() {
localStorage.removeItem("token");
clearAllStores();
if (user?.type === UserType.Proxy) {
// Proxy users logout flow is different.
proxyUserLogoutShown = true;
return;
}
clearWatcharrData();
goto("/login");
}
@@ -365,6 +372,9 @@
{/if}
{/if}
<button class="plain" on:click={() => logout()}>Logout</button>
{#if proxyUserLogoutShown}
<ProxyUserLogoutModal onClose={() => (proxyUserLogoutShown = false)} />
{/if}
<!-- svelte-ignore missing-declaration -->
<span>v{__WATCHARR_VERSION__}</span>
</div>
@@ -92,7 +92,7 @@
<Setting title="Logout URL" desc="Where can we redirect so that the user can logout?">
<input
type="text"
placeholder="https://auth.example.com"
placeholder="https://auth.example.com/logout"
on:blur={() => {}}
disabled={formDisabled}
bind:value={headerCfg.logoutUrl}
+6 -1
View File
@@ -14,6 +14,7 @@
let apPlex = false;
let signupEnabled = true;
let useEmby = false;
let noAuto = false;
onMount(() => {
if (localStorage.getItem("token")) {
@@ -31,7 +32,7 @@
apPlex = availableProviders?.includes("plex");
signupEnabled = r.data.signupEnabled;
useEmby = r.data.useEmby;
if (r.data.headerAuthAutoLogin) {
if (r.data.headerAuthAutoLogin && !noAuto) {
console.log("handling headerAuthAutoLogin.. calling proxyLogin automatically now.");
proxyLogin(true);
}
@@ -43,6 +44,10 @@
if (!error && $page.url.searchParams.get("again")) {
error = "Please Login Again";
}
if ($page.url.searchParams.get("noAuto") == "1") {
console.info("login: Found noAuto param.. auto logins should be disabled now.");
noAuto = true;
}
});
function handleLogin(ev: SubmitEvent) {
+6 -1
View File
@@ -69,7 +69,8 @@ export type PosterExtraDetails = {
export enum UserType {
// Assume watcharr user if none of these...
Jellyfin = 1,
Plex = 2
Plex = 2,
Proxy = 3
}
interface dbModel {
@@ -970,6 +971,10 @@ export interface TrustedHeaderAuthSetting {
logoutUrl?: string;
}
export interface TrustedHeaderAuthLogoutDetailsResponse {
logoutUrl?: string;
}
export interface DropDownItem {
id: number | string;
value: string;