Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to disable local auth form #2752

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cli // import "miniflux.app/v2/internal/cli"

import (
"errors"
"flag"
"fmt"
"io"
Expand Down Expand Up @@ -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)
}

Expand Down
9 changes: 9 additions & 0 deletions internal/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const (
defaultOAuth2RedirectURL = ""
defaultOAuth2OidcDiscoveryEndpoint = ""
defaultOAuth2Provider = ""
defaultDisableLocalAuth = false
defaultPocketConsumerKey = ""
defaultHTTPClientTimeout = 20
defaultHTTPClientMaxBodySize = 15
Expand Down Expand Up @@ -154,6 +155,7 @@ type Options struct {
oauth2RedirectURL string
oidcDiscoveryEndpoint string
oauth2Provider string
disableLocalAuth bool
pocketConsumerKey string
httpClientTimeout int
httpClientMaxBodySize int64
Expand Down Expand Up @@ -231,6 +233,7 @@ func NewOptions() *Options {
oauth2RedirectURL: defaultOAuth2RedirectURL,
oidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
oauth2Provider: defaultOAuth2Provider,
disableLocalAuth: defaultDisableLocalAuth,
pocketConsumerKey: defaultPocketConsumerKey,
httpClientTimeout: defaultHTTPClientTimeout,
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions internal/config/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
15 changes: 8 additions & 7 deletions internal/template/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
2 changes: 2 additions & 0 deletions internal/template/templates/views/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}">

Expand All @@ -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">
Expand Down
2 changes: 2 additions & 0 deletions internal/template/templates/views/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1 id="page-header-title">{{ t "page.settings.title" }}</h1>
<div role="alert" class="alert alert-error">{{ .errorMessage }}</div>
{{ end }}

{{ if not disableLocalAuth }}
<fieldset>
<legend>{{ t "form.prefs.fieldset.authentication_settings" }}</legend>

Expand Down Expand Up @@ -49,6 +50,7 @@ <h1 id="page-header-title">{{ t "page.settings.title" }}</h1>
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
</fieldset>
{{ end }}

{{ if .webAuthnEnabled }}
<fieldset>
Expand Down
7 changes: 5 additions & 2 deletions internal/ui/form/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"strconv"

"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/model"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
}

Expand Down
13 changes: 11 additions & 2 deletions internal/ui/login_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
9 changes: 9 additions & 0 deletions internal/ui/oauth2_unlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 == "" {
Expand Down
8 changes: 8 additions & 0 deletions miniflux.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading