aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/cli/cli.go12
-rw-r--r--internal/config/options.go9
-rw-r--r--internal/config/parser.go2
-rw-r--r--internal/template/functions.go15
-rw-r--r--internal/template/templates/views/login.html2
-rw-r--r--internal/template/templates/views/settings.html2
-rw-r--r--internal/ui/form/settings.go7
-rw-r--r--internal/ui/login_check.go13
-rw-r--r--internal/ui/oauth2_unlink.go9
-rw-r--r--miniflux.18
10 files changed, 68 insertions, 11 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index f56bb959..56cf13fa 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,17 @@ func Parse() {
return
}
+ if config.Opts.DisableLocalAuth() {
+ 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"))
+ 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"))
+ 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"))
+ }
+ }
+
startDaemon(store)
}
diff --git a/internal/config/options.go b/internal/config/options.go
index d2536070..484f5f9c 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 9ea4053c..062ffea8 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 cfbfc53d..4a91ffdf 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 56b8ac74..14cb791f 100644
--- a/internal/template/templates/views/login.html
+++ b/internal/template/templates/views/login.html
@@ -5,6 +5,7 @@
{{ define "content"}}
<section class="login-form">
+ {{ if not disableLocalAuth }}
<form action="{{ route "checkLogin" }}" method="post">
<input type="hidden" name="csrf" value="{{ .csrf }}">
@@ -22,6 +23,7 @@
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.loading" }}">{{ t "action.login" }}</button>
</div>
</form>
+ {{ end }}
{{ if .webAuthnEnabled }}
<div class="webauthn">
<div role="alert" class="alert alert-error hidden" id="webauthn-error">
diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html
index ce60a808..58ad67d1 100644
--- a/internal/template/templates/views/settings.html
+++ b/internal/template/templates/views/settings.html
@@ -15,6 +15,7 @@
<div role="alert" class="alert alert-error">{{ .errorMessage }}</div>
{{ end }}
+ {{ if not disableLocalAuth }}
<fieldset>
<legend>{{ t "form.prefs.fieldset.authentication_settings" }}</legend>
@@ -49,6 +50,7 @@
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
</fieldset>
+ {{ end }}
{{ if .webAuthnEnabled }}
<fieldset>
diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go
index b4ac41cc..8b79ba88 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 b870cf57..eb3c6720 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 83b1fcfa..e10cb5a4 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 4edd0ff7..48868d0b 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