From b9ac45d5dac4b4eb2ba364629090fbf306cffd2b Mon Sep 17 00:00:00 2001 From: jake-dog Date: Sun, 30 Nov 2025 02:44:34 -0500 Subject: [PATCH] feat: configurable logout page URL for proxy/hook auth (#3884) Co-authored-by: Henrique Dias --- cmd/config.go | 4 ++++ frontend/index.html | 1 + frontend/src/components/Sidebar.vue | 3 ++- frontend/src/utils/auth.ts | 10 +++++++++- frontend/src/utils/constants.ts | 2 ++ http/auth.go | 28 +++++++++++++++++++++------- http/static.go | 1 + settings/settings.go | 2 ++ settings/storage.go | 10 ++++++++++ 9 files changed, 52 insertions(+), 9 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 550ab5c9..93ba29df 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -45,6 +45,7 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type") flags.String("auth.header", "", "HTTP header for auth.method=proxy") flags.String("auth.command", "", "command for auth.method=hook") + flags.String("auth.logoutPage", "", "url of custom logout page") flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China") flags.String("recaptcha.key", "", "ReCaptcha site key") @@ -201,6 +202,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup) fmt.Fprintf(w, "Hide Login Button:\t%t\n", set.HideLoginButton) fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir) + fmt.Fprintf(w, "Logout Page:\t%s\n", set.LogoutPage) fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength) fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod) fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " ")) @@ -328,6 +330,8 @@ func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Ser set.DirMode, err = getAndParseFileMode(flags, flag.Name) case "auth.method": hasAuth = true + case "auth.logoutPage": + set.LogoutPage, err = flags.GetString(flag.Name) case "branding.name": set.Branding.Name, err = flags.GetString(flag.Name) case "branding.theme": diff --git a/frontend/index.html b/frontend/index.html index b3de6804..19308a95 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -39,6 +39,7 @@ DisableUsedPercentage: false, EnableExec: true, EnableThumbs: true, + LogoutPage: "", LoginPage: true, Name: "", NoAuth: false, diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 531ec583..c7961e3e 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -129,6 +129,7 @@ import { disableExternal, disableUsedPercentage, noAuth, + logoutPage, loginPage, } from "@/utils/constants"; import { files as api } from "@/api"; @@ -159,7 +160,7 @@ export default { version: () => version, disableExternal: () => disableExternal, disableUsedPercentage: () => disableUsedPercentage, - canLogout: () => !noAuth && loginPage, + canLogout: () => !noAuth && (loginPage || logoutPage !== "/login"), }, methods: { ...mapActions(useLayoutStore, ["closeHovers", "showHover"]), diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts index dcd1cdc5..e75714f2 100644 --- a/frontend/src/utils/auth.ts +++ b/frontend/src/utils/auth.ts @@ -2,7 +2,7 @@ import { useAuthStore } from "@/stores/auth"; import router from "@/router"; import type { JwtPayload } from "jwt-decode"; import { jwtDecode } from "jwt-decode"; -import { baseURL, noAuth } from "./constants"; +import { authMethod, baseURL, noAuth, logoutPage } from "./constants"; import { StatusError } from "@/api/utils"; import { setSafeTimeout } from "@/api/utils"; @@ -18,6 +18,12 @@ export function parseToken(token: string) { authStore.jwt = token; authStore.setUser(data.user); + // proxy auth with custom logout subject to unknown external timeout + if (logoutPage !== "/login" && authMethod === "proxy") { + console.warn("idle timeout disabled with proxy auth and custom logout"); + return; + } + if (authStore.logoutTimer) { clearTimeout(authStore.logoutTimer); } @@ -118,6 +124,8 @@ export function logout(reason?: string) { localStorage.setItem("jwt", ""); if (noAuth) { window.location.reload(); + } else if (logoutPage !== "/login") { + document.location.href = `${logoutPage}`; } else { if (typeof reason === "string" && reason.trim() !== "") { router.push({ diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index ae491397..0580205a 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -10,6 +10,7 @@ const version: string = window.FileBrowser.Version; const logoURL = `${staticURL}/img/logo.svg`; const noAuth: boolean = window.FileBrowser.NoAuth; const authMethod = window.FileBrowser.AuthMethod; +const logoutPage: string = window.FileBrowser.LogoutPage; const loginPage: boolean = window.FileBrowser.LoginPage; const theme: UserTheme = window.FileBrowser.Theme; const enableThumbs: boolean = window.FileBrowser.EnableThumbs; @@ -32,6 +33,7 @@ export { version, noAuth, authMethod, + logoutPage, loginPage, theme, enableThumbs, diff --git a/http/auth.go b/http/auth.go index 2e4518f0..4c052a5c 100644 --- a/http/auth.go +++ b/http/auth.go @@ -12,7 +12,9 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5/request" + fbAuth "github.com/filebrowser/filebrowser/v2/auth" fbErrors "github.com/filebrowser/filebrowser/v2/errors" + "github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/users" ) @@ -61,6 +63,22 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) { return "", request.ErrNoTokenInRequest } +func renewableErr(err error, d *data) bool { + if d.settings.AuthMethod != fbAuth.MethodProxyAuth || err == nil { + return false + } + + if d.settings.LogoutPage == settings.DefaultLogoutPage { + return false + } + + if !errors.Is(err, jwt.ErrTokenExpired) { + return false + } + + return true +} + func withUser(fn handleFunc) handleFunc { return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { keyFunc := func(_ *jwt.Token) (interface{}, error) { @@ -68,13 +86,9 @@ func withUser(fn handleFunc) handleFunc { } var tk authToken - token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk)) - if err != nil || !token.Valid { - return http.StatusUnauthorized, nil - } - - err = jwt.NewValidator(jwt.WithExpirationRequired()).Validate(tk) - if err != nil { + p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithExpirationRequired()) + token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk), request.WithParser(p)) + if (err != nil || !token.Valid) && !renewableErr(err, d) { return http.StatusUnauthorized, nil } diff --git a/http/static.go b/http/static.go index be2c135c..b8883c4e 100644 --- a/http/static.go +++ b/http/static.go @@ -38,6 +38,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys "Signup": d.settings.Signup, "NoAuth": d.settings.AuthMethod == auth.MethodNoAuth, "AuthMethod": d.settings.AuthMethod, + "LogoutPage": d.settings.LogoutPage, "LoginPage": auther.LoginPage(), "CSS": false, "ReCaptcha": false, diff --git a/settings/settings.go b/settings/settings.go index c263bf36..6bcc36f8 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -11,6 +11,7 @@ import ( ) const DefaultUsersHomeBasePath = "/users" +const DefaultLogoutPage = "/login" const DefaultMinimumPasswordLength = 12 const DefaultFileMode = 0640 const DefaultDirMode = 0750 @@ -27,6 +28,7 @@ type Settings struct { UserHomeBasePath string `json:"userHomeBasePath"` Defaults UserDefaults `json:"defaults"` AuthMethod AuthMethod `json:"authMethod"` + LogoutPage string `json:"logoutPage"` Branding Branding `json:"branding"` Tus Tus `json:"tus"` Commands map[string][]string `json:"commands"` diff --git a/settings/storage.go b/settings/storage.go index d401996d..8bf11a64 100644 --- a/settings/storage.go +++ b/settings/storage.go @@ -30,24 +30,34 @@ func (s *Storage) Get() (*Settings, error) { if err != nil { return nil, err } + if set.UserHomeBasePath == "" { set.UserHomeBasePath = DefaultUsersHomeBasePath } + + if set.LogoutPage == "" { + set.LogoutPage = DefaultLogoutPage + } + if set.MinimumPasswordLength == 0 { set.MinimumPasswordLength = DefaultMinimumPasswordLength } + if set.Tus == (Tus{}) { set.Tus = Tus{ ChunkSize: DefaultTusChunkSize, RetryCount: DefaultTusRetryCount, } } + if set.FileMode == 0 { set.FileMode = DefaultFileMode } + if set.DirMode == 0 { set.DirMode = DefaultDirMode } + return set, nil }