From 048185966041ac44cb65251afd9ea5321b672238 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 17 Jul 2024 19:20:14 -0700 Subject: [PATCH 1/2] feat: Add option to disable local auth form (#2649, #2220) --- internal/cli/cli.go | 11 +++++++++++ internal/config/options.go | 9 +++++++++ internal/config/parser.go | 2 ++ internal/template/functions.go | 15 ++++++++------- internal/template/templates/views/login.html | 2 ++ internal/template/templates/views/settings.html | 2 ++ internal/ui/form/settings.go | 7 +++++-- internal/ui/login_check.go | 13 +++++++++++-- internal/ui/oauth2_unlink.go | 9 +++++++++ miniflux.1 | 8 ++++++++ 10 files changed, 67 insertions(+), 11 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index f56bb959a87..941c6bb6e05 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -4,6 +4,7 @@ package cli // import "miniflux.app/v2/internal/cli" import ( + "errors" "flag" "fmt" "io" @@ -225,6 +226,16 @@ func Parse() { return } + if config.Opts.DisableLocalAuth() { + if config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "" { + printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled but neither OAUTH2_PROVIDER nor AUTH_PROXY_HEADER is not set. Please enable at least one authentication source")) + } else if config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed() { + printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an OAUTH2_PROVIDER is configured, but OAUTH2_USER_CREATION is not enabled")) + } else if config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed() { + printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an AUTH_PROXY_HEADER is configured, but AUTH_PROXY_USER_CREATION is not enabled")) + } + } + startDaemon(store) } diff --git a/internal/config/options.go b/internal/config/options.go index d2536070b6d..484f5f9ce3d 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -70,6 +70,7 @@ const ( defaultOAuth2RedirectURL = "" defaultOAuth2OidcDiscoveryEndpoint = "" defaultOAuth2Provider = "" + defaultDisableLocalAuth = false defaultPocketConsumerKey = "" defaultHTTPClientTimeout = 20 defaultHTTPClientMaxBodySize = 15 @@ -154,6 +155,7 @@ type Options struct { oauth2RedirectURL string oidcDiscoveryEndpoint string oauth2Provider string + disableLocalAuth bool pocketConsumerKey string httpClientTimeout int httpClientMaxBodySize int64 @@ -231,6 +233,7 @@ func NewOptions() *Options { oauth2RedirectURL: defaultOAuth2RedirectURL, oidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint, oauth2Provider: defaultOAuth2Provider, + disableLocalAuth: defaultDisableLocalAuth, pocketConsumerKey: defaultPocketConsumerKey, httpClientTimeout: defaultHTTPClientTimeout, httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024, @@ -456,6 +459,11 @@ func (o *Options) OAuth2Provider() string { return o.oauth2Provider } +// DisableLocalAUth returns true if the local user database should not be used to authenticate users +func (o *Options) DisableLocalAuth() bool { + return o.disableLocalAuth +} + // HasHSTS returns true if HTTP Strict Transport Security is enabled. func (o *Options) HasHSTS() bool { return o.hsts @@ -695,6 +703,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option { "OAUTH2_PROVIDER": o.oauth2Provider, "OAUTH2_REDIRECT_URL": o.oauth2RedirectURL, "OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed, + "DISABLE_LOCAL_AUTH": o.disableLocalAuth, "POCKET_CONSUMER_KEY": redactSecretValue(o.pocketConsumerKey, redactSecret), "POLLING_FREQUENCY": o.pollingFrequency, "FORCE_REFRESH_INTERVAL": o.forceRefreshInterval, diff --git a/internal/config/parser.go b/internal/config/parser.go index 9ea4053cb43..062ffea8b8a 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -227,6 +227,8 @@ func (p *Parser) parseLines(lines []string) (err error) { p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint) case "OAUTH2_PROVIDER": p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider) + case "DISABLE_LOCAL_AUTH": + p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth) case "HTTP_CLIENT_TIMEOUT": p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout) case "HTTP_CLIENT_MAX_BODY_SIZE": diff --git a/internal/template/functions.go b/internal/template/functions.go index cfbfc53d785..4a91ffdf8a2 100644 --- a/internal/template/functions.go +++ b/internal/template/functions.go @@ -32,13 +32,14 @@ type funcMap struct { // Map returns a map of template functions that are compiled during template parsing. func (f *funcMap) Map() template.FuncMap { return template.FuncMap{ - "formatFileSize": formatFileSize, - "dict": dict, - "hasKey": hasKey, - "truncate": truncate, - "isEmail": isEmail, - "baseURL": config.Opts.BaseURL, - "rootURL": config.Opts.RootURL, + "formatFileSize": formatFileSize, + "dict": dict, + "hasKey": hasKey, + "truncate": truncate, + "isEmail": isEmail, + "baseURL": config.Opts.BaseURL, + "rootURL": config.Opts.RootURL, + "disableLocalAuth": config.Opts.DisableLocalAuth, "hasOAuth2Provider": func(provider string) bool { return config.Opts.OAuth2Provider() == provider }, diff --git a/internal/template/templates/views/login.html b/internal/template/templates/views/login.html index 56b8ac74602..14cb791fa67 100644 --- a/internal/template/templates/views/login.html +++ b/internal/template/templates/views/login.html @@ -5,6 +5,7 @@ {{ define "content"}}
+ {{ if not disableLocalAuth }}
@@ -22,6 +23,7 @@
+ {{ end }} {{ if .webAuthnEnabled }}
+ {{ end }} {{ if .webAuthnEnabled }}
diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go index b4ac41cc0c1..8b79ba88e66 100644 --- a/internal/ui/form/settings.go +++ b/internal/ui/form/settings.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" + "miniflux.app/v2/internal/config" "miniflux.app/v2/internal/locale" "miniflux.app/v2/internal/model" ) @@ -86,7 +87,9 @@ func ExtractMarkAsReadBehavior(behavior MarkReadBehavior) (markReadOnView, markR // Merge updates the fields of the given user. func (s *SettingsForm) Merge(user *model.User) *model.User { - user.Username = s.Username + if !config.Opts.DisableLocalAuth() { + user.Username = s.Username + } user.Theme = s.Theme user.Language = s.Language user.Timezone = s.Timezone @@ -120,7 +123,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User { // Validate makes sure the form values are valid. func (s *SettingsForm) Validate() *locale.LocalizedError { - if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" || s.DisplayMode == "" || s.DefaultHomePage == "" { + if (s.Username == "" && !config.Opts.DisableLocalAuth()) || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" || s.DisplayMode == "" || s.DefaultHomePage == "" { return locale.NewLocalizedError("error.settings_mandatory_fields") } diff --git a/internal/ui/login_check.go b/internal/ui/login_check.go index b870cf5739e..eb3c672041b 100644 --- a/internal/ui/login_check.go +++ b/internal/ui/login_check.go @@ -21,9 +21,18 @@ import ( func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) { clientIP := request.ClientIP(r) sess := session.New(h.store, request.SessionID(r)) - authForm := form.NewAuthForm(r) - view := view.New(h.tpl, r, sess) + + if config.Opts.DisableLocalAuth() { + slog.Warn("blocking local auth login attempt, local auth is disabled", + slog.String("client_ip", clientIP), + slog.String("user_agent", r.UserAgent()), + ) + html.OK(w, r, view.Render("login")) + return + } + + authForm := form.NewAuthForm(r) view.Set("errorMessage", locale.NewLocalizedError("error.bad_credentials").Translate(request.UserLanguage(r))) view.Set("form", authForm) diff --git a/internal/ui/oauth2_unlink.go b/internal/ui/oauth2_unlink.go index 83b1fcfa361..e10cb5a459f 100644 --- a/internal/ui/oauth2_unlink.go +++ b/internal/ui/oauth2_unlink.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" + "miniflux.app/v2/internal/config" "miniflux.app/v2/internal/http/request" "miniflux.app/v2/internal/http/response/html" "miniflux.app/v2/internal/http/route" @@ -15,6 +16,14 @@ import ( ) func (h *handler) oauth2Unlink(w http.ResponseWriter, r *http.Request) { + if config.Opts.DisableLocalAuth() { + slog.Warn("blocking oauth2 unlink attempt, local auth is disabled", + slog.String("user_agent", r.UserAgent()), + ) + html.Redirect(w, r, route.Path(h.router, "login")) + return + } + printer := locale.NewPrinter(request.UserLanguage(r)) provider := request.RouteStringParam(r, "provider") if provider == "" { diff --git a/miniflux.1 b/miniflux.1 index 4edd0ff740c..48868d0b69e 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -447,6 +447,14 @@ Possible values are "google" or "oidc"\&. .br Default is empty\&. .TP +.B DISABLE_LOCAL_AUTH +Only use oauth2 for auth\&. +.br +When set to true, the username/password form is hidden from the login screen, and the +options to change username/password or unlink oauth2 account are hidden from the settings page. +.br +Default is false\&. +.TP .B OAUTH2_REDIRECT_URL OAuth2 redirect URL\&. .br From ee2230601734aa771ab1894658ac3ac58fa8344e Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 8 Aug 2024 16:10:26 -0700 Subject: [PATCH 2/2] address linter concerns --- internal/cli/cli.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 941c6bb6e05..56cf13fa363 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -227,11 +227,12 @@ func Parse() { } if config.Opts.DisableLocalAuth() { - if config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "" { + switch { + case config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "": printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled but neither OAUTH2_PROVIDER nor AUTH_PROXY_HEADER is not set. Please enable at least one authentication source")) - } else if config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed() { + case config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed(): printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an OAUTH2_PROVIDER is configured, but OAUTH2_USER_CREATION is not enabled")) - } else if config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed() { + case config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed(): printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an AUTH_PROXY_HEADER is configured, but AUTH_PROXY_USER_CREATION is not enabled")) } }