mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
sso logout flow
This commit is contained in:
@@ -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
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user