mirror of
https://github.com/miniflux/v2.git
synced 2024-09-27 21:02:41 +02:00
Implement support for authentication via Auth Proxy
Auth Proxy allows to authenticate a user using an HTTP header provided by an external authentication service. This provides a way to authenticate users in miniflux using authentication schemes not supported by miniflux itself (LDAP, non-Google OAuth2 providers, etc.) and to implement SSO for multiple applications behind single authentication service. Auth Proxy header is checked for the '/' endpoint only, as the rest are protected by the miniflux user/app sessions. Closes #534 Signed-off-by: Pavel Borzenkov <pavel.borzenkov@gmail.com>
This commit is contained in:
parent
d5adf8b9f6
commit
7389c79c52
@ -1257,3 +1257,73 @@ Invalid text
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthProxyHeader(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
os.Setenv("AUTH_PROXY_HEADER", "X-Forwarded-User")
|
||||||
|
|
||||||
|
parser := NewParser()
|
||||||
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "X-Forwarded-User"
|
||||||
|
result := opts.AuthProxyHeader()
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf(`Unexpected AUTH_PROXY_HEADER value, got %q instead of %q`, result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultAuthProxyHeaderValue(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
|
||||||
|
parser := NewParser()
|
||||||
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := defaultAuthProxyHeader
|
||||||
|
result := opts.AuthProxyHeader()
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf(`Unexpected AUTH_PROXY_HEADER value, got %q instead of %q`, result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthProxyUserCreationWhenUnset(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
|
||||||
|
parser := NewParser()
|
||||||
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := false
|
||||||
|
result := opts.IsAuthProxyUserCreationAllowed()
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf(`Unexpected AUTH_PROXY_USER_CREATION value, got %v instead of %v`, result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthProxyUserCreationAdmin(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
os.Setenv("AUTH_PROXY_USER_CREATION", "1")
|
||||||
|
|
||||||
|
parser := NewParser()
|
||||||
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := true
|
||||||
|
result := opts.IsAuthProxyUserCreationAllowed()
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf(`Unexpected AUTH_PROXY_USER_CREATION value, got %v instead of %v`, result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -44,6 +44,8 @@ const (
|
|||||||
defaultPocketConsumerKey = ""
|
defaultPocketConsumerKey = ""
|
||||||
defaultHTTPClientTimeout = 20
|
defaultHTTPClientTimeout = 20
|
||||||
defaultHTTPClientMaxBodySize = 15
|
defaultHTTPClientMaxBodySize = 15
|
||||||
|
defaultAuthProxyHeader = ""
|
||||||
|
defaultAuthProxyUserCreation = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options contains configuration options.
|
// Options contains configuration options.
|
||||||
@ -82,6 +84,8 @@ type Options struct {
|
|||||||
pocketConsumerKey string
|
pocketConsumerKey string
|
||||||
httpClientTimeout int
|
httpClientTimeout int
|
||||||
httpClientMaxBodySize int64
|
httpClientMaxBodySize int64
|
||||||
|
authProxyHeader string
|
||||||
|
authProxyUserCreation bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions returns Options with default values.
|
// NewOptions returns Options with default values.
|
||||||
@ -121,6 +125,8 @@ func NewOptions() *Options {
|
|||||||
pocketConsumerKey: defaultPocketConsumerKey,
|
pocketConsumerKey: defaultPocketConsumerKey,
|
||||||
httpClientTimeout: defaultHTTPClientTimeout,
|
httpClientTimeout: defaultHTTPClientTimeout,
|
||||||
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
||||||
|
authProxyHeader: defaultAuthProxyHeader,
|
||||||
|
authProxyUserCreation: defaultAuthProxyUserCreation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,6 +303,18 @@ func (o *Options) HTTPClientMaxBodySize() int64 {
|
|||||||
return o.httpClientMaxBodySize
|
return o.httpClientMaxBodySize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthProxyHeader returns an HTTP header name that contains username for
|
||||||
|
// authentication using auth proxy.
|
||||||
|
func (o *Options) AuthProxyHeader() string {
|
||||||
|
return o.authProxyHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAuthProxyUserCreationAllowed returns true if user creation is allowed for
|
||||||
|
// users authenticated using auth proxy.
|
||||||
|
func (o *Options) IsAuthProxyUserCreationAllowed() bool {
|
||||||
|
return o.authProxyUserCreation
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Options) String() string {
|
func (o *Options) String() string {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString(fmt.Sprintf("LOG_DATE_TIME: %v\n", o.logDateTime))
|
builder.WriteString(fmt.Sprintf("LOG_DATE_TIME: %v\n", o.logDateTime))
|
||||||
@ -333,5 +351,7 @@ func (o *Options) String() string {
|
|||||||
builder.WriteString(fmt.Sprintf("OAUTH2_PROVIDER: %v\n", o.oauth2Provider))
|
builder.WriteString(fmt.Sprintf("OAUTH2_PROVIDER: %v\n", o.oauth2Provider))
|
||||||
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_TIMEOUT: %v\n", o.httpClientTimeout))
|
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_TIMEOUT: %v\n", o.httpClientTimeout))
|
||||||
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_MAX_BODY_SIZE: %v\n", o.httpClientMaxBodySize))
|
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_MAX_BODY_SIZE: %v\n", o.httpClientMaxBodySize))
|
||||||
|
builder.WriteString(fmt.Sprintf("AUTH_PROXY_HEADER: %v\n", o.authProxyHeader))
|
||||||
|
builder.WriteString(fmt.Sprintf("AUTH_PROXY_USER_CREATION: %v\n", o.authProxyUserCreation))
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
@ -158,6 +158,10 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||||||
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
||||||
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
||||||
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
|
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
|
||||||
|
case "AUTH_PROXY_HEADER":
|
||||||
|
p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
|
||||||
|
case "AUTH_PROXY_USER_CREATION":
|
||||||
|
p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"miniflux.app/logger"
|
"miniflux.app/logger"
|
||||||
"miniflux.app/model"
|
"miniflux.app/model"
|
||||||
"miniflux.app/storage"
|
"miniflux.app/storage"
|
||||||
|
"miniflux.app/ui/session"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
@ -155,3 +156,66 @@ func (m *middleware) getUserSessionFromCookie(r *http.Request) *model.UserSessio
|
|||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if request.IsAuthenticated(r) || config.Opts.AuthProxyHeader() == "" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := r.Header.Get(config.Opts.AuthProxyHeader())
|
||||||
|
if username == "" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(m.store, request.SessionID(r))
|
||||||
|
clientIP := request.ClientIP(r)
|
||||||
|
|
||||||
|
logger.Info("[AuthProxy] Successful auth for %s", username)
|
||||||
|
|
||||||
|
user, err := m.store.UserByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
if !config.Opts.IsAuthProxyUserCreationAllowed() {
|
||||||
|
html.Forbidden(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user = model.NewUser()
|
||||||
|
user.Username = username
|
||||||
|
user.IsAdmin = false
|
||||||
|
|
||||||
|
if err := m.store.CreateUser(user); err != nil {
|
||||||
|
html.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionToken, _, err := m.store.CreateUserSession(user.Username, r.UserAgent(), clientIP)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("[AuthProxy] username=%s just logged in", user.Username)
|
||||||
|
|
||||||
|
m.store.SetLastLogin(user.ID)
|
||||||
|
sess.SetLanguage(user.Language)
|
||||||
|
sess.SetTheme(user.Theme)
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie.New(
|
||||||
|
cookie.CookieUserSessionID,
|
||||||
|
sessionToken,
|
||||||
|
config.Opts.HTTPS,
|
||||||
|
config.Opts.BasePath(),
|
||||||
|
))
|
||||||
|
|
||||||
|
html.Redirect(w, r, route.Path(m.router, "unread"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
2
ui/ui.go
2
ui/ui.go
@ -123,7 +123,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
|
|||||||
// Authentication pages.
|
// Authentication pages.
|
||||||
uiRouter.HandleFunc("/login", handler.checkLogin).Name("checkLogin").Methods("POST")
|
uiRouter.HandleFunc("/login", handler.checkLogin).Name("checkLogin").Methods("POST")
|
||||||
uiRouter.HandleFunc("/logout", handler.logout).Name("logout").Methods("GET")
|
uiRouter.HandleFunc("/logout", handler.logout).Name("logout").Methods("GET")
|
||||||
uiRouter.HandleFunc("/", handler.showLoginPage).Name("login").Methods("GET")
|
uiRouter.Handle("/", middleware.handleAuthProxy(http.HandlerFunc(handler.showLoginPage))).Name("login").Methods("GET")
|
||||||
|
|
||||||
router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
Loading…
Reference in New Issue
Block a user