mirror of
https://github.com/rclone/rclone.git
synced 2025-02-08 06:29:22 +01:00
http servers: add --user-from-header to use for authentication
Retrieve the username from a specified HTTP header if no other authentication methods are configured (ideal for proxied setups)
This commit is contained in:
parent
bf5a4774c6
commit
347be176af
@ -19,7 +19,11 @@ By default this will serve files without needing a login.
|
||||
You can either use an htpasswd file which can take lots of users, or
|
||||
set a single username and password with the ` + "`--{{ .Prefix }}user` and `--{{ .Prefix }}pass`" + ` flags.
|
||||
|
||||
If no static users are configured by either of the above methods, and client
|
||||
Alternatively, you can have the reverse proxy manage authentication and use the
|
||||
username provided in the configured header with ` + "`--user-from-header`" + ` (e.g., ` + "`--{{ .Prefix }}--user-from-header=x-remote-user`" + `).
|
||||
Ensure the proxy is trusted and headers cannot be spoofed, as misconfiguration may lead to unauthorized access.
|
||||
|
||||
If either of the above authentication methods is not configured and client
|
||||
certificates are required by the ` + "`--client-ca`" + ` flag passed to the server, the
|
||||
client certificate common name will be considered as the username.
|
||||
|
||||
@ -85,16 +89,21 @@ var AuthConfigInfo = fs.Options{{
|
||||
Name: "salt",
|
||||
Default: "dlPL2MqE",
|
||||
Help: "Password hashing salt",
|
||||
}, {
|
||||
Name: "user_from_header",
|
||||
Default: "",
|
||||
Help: "User name from a defined HTTP header",
|
||||
}}
|
||||
|
||||
// AuthConfig contains options for the http authentication
|
||||
type AuthConfig struct {
|
||||
HtPasswd string `config:"htpasswd"` // htpasswd file - if not provided no authentication is done
|
||||
Realm string `config:"realm"` // realm for authentication
|
||||
BasicUser string `config:"user"` // single username for basic auth if not using Htpasswd
|
||||
BasicPass string `config:"pass"` // password for BasicUser
|
||||
Salt string `config:"salt"` // password hashing salt
|
||||
CustomAuthFn CustomAuthFn `json:"-" config:"-"` // custom Auth (not set by command line flags)
|
||||
HtPasswd string `config:"htpasswd"` // htpasswd file - if not provided no authentication is done
|
||||
Realm string `config:"realm"` // realm for authentication
|
||||
BasicUser string `config:"user"` // single username for basic auth if not using Htpasswd
|
||||
BasicPass string `config:"pass"` // password for BasicUser
|
||||
Salt string `config:"salt"` // password hashing salt
|
||||
UserFromHeader string `config:"user_from_header"` // retrieve user name from a defined HTTP header
|
||||
CustomAuthFn CustomAuthFn `json:"-" config:"-"` // custom Auth (not set by command line flags)
|
||||
}
|
||||
|
||||
// AddFlagsPrefix adds flags to the flag set for AuthConfig
|
||||
@ -104,6 +113,7 @@ func (cfg *AuthConfig) AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string) {
|
||||
flags.StringVarP(flagSet, &cfg.BasicUser, prefix+"user", "", cfg.BasicUser, "User name for authentication", prefix)
|
||||
flags.StringVarP(flagSet, &cfg.BasicPass, prefix+"pass", "", cfg.BasicPass, "Password for authentication", prefix)
|
||||
flags.StringVarP(flagSet, &cfg.Salt, prefix+"salt", "", cfg.Salt, "Password hashing salt", prefix)
|
||||
flags.StringVarP(flagSet, &cfg.UserFromHeader, prefix+"user-from-header", "", cfg.UserFromHeader, "Retrieve the username from a specified HTTP header if no other authentication methods are configured (ideal for proxied setups)", prefix)
|
||||
}
|
||||
|
||||
// AddAuthFlagsPrefix adds flags to the flag set for AuthConfig
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -153,6 +154,26 @@ func MiddlewareAuthCustom(fn CustomAuthFn, realm string, userFromContext bool) M
|
||||
}
|
||||
}
|
||||
|
||||
var validUsernameRegexp = regexp.MustCompile(`^[\p{L}\d@._-]+$`)
|
||||
|
||||
// MiddlewareAuthGetUserFromHeader middleware that bypasses authentication and extracts the user via a specified HTTP header(ideal for proxied setups).
|
||||
func MiddlewareAuthGetUserFromHeader(header string) Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
username := strings.TrimSpace(r.Header.Get(header))
|
||||
if username != "" && validUsernameRegexp.MatchString(username) {
|
||||
r = r.WithContext(context.WithValue(r.Context(), ctxKeyUser, username))
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
code := http.StatusUnauthorized
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.Error(w, http.StatusText(code), code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var onlyOnceWarningAllowOrigin sync.Once
|
||||
|
||||
// MiddlewareCORS instantiates middleware that handles basic CORS protections for rcd
|
||||
|
@ -14,11 +14,13 @@ import (
|
||||
|
||||
func TestMiddlewareAuth(t *testing.T) {
|
||||
servers := []struct {
|
||||
name string
|
||||
http Config
|
||||
auth AuthConfig
|
||||
user string
|
||||
pass string
|
||||
name string
|
||||
expectedUser string
|
||||
remoteUser string
|
||||
http Config
|
||||
auth AuthConfig
|
||||
user string
|
||||
pass string
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
@ -85,9 +87,32 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
},
|
||||
user: "custom",
|
||||
pass: "custom",
|
||||
}, {
|
||||
name: "UserFromHeader",
|
||||
remoteUser: "remoteUser",
|
||||
expectedUser: "remoteUser",
|
||||
http: Config{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
},
|
||||
auth: AuthConfig{
|
||||
UserFromHeader: "X-Remote-User",
|
||||
},
|
||||
}, {
|
||||
name: "UserFromHeader/MixedWithHtPasswd",
|
||||
remoteUser: "remoteUser",
|
||||
expectedUser: "md5",
|
||||
http: Config{
|
||||
ListenAddr: []string{"127.0.0.1:0"},
|
||||
},
|
||||
auth: AuthConfig{
|
||||
UserFromHeader: "X-Remote-User",
|
||||
Realm: "test",
|
||||
HtPasswd: "./testdata/.htpasswd",
|
||||
},
|
||||
user: "md5",
|
||||
pass: "md5",
|
||||
},
|
||||
}
|
||||
|
||||
for _, ss := range servers {
|
||||
t.Run(ss.name, func(t *testing.T) {
|
||||
s, err := NewServer(context.Background(), WithConfig(ss.http), WithAuth(ss.auth))
|
||||
@ -97,7 +122,12 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
}()
|
||||
|
||||
expected := []byte("secret-page")
|
||||
s.Router().Mount("/", testEchoHandler(expected))
|
||||
if ss.expectedUser != "" {
|
||||
s.Router().Mount("/", testAuthUserHandler())
|
||||
} else {
|
||||
s.Router().Mount("/", testEchoHandler(expected))
|
||||
}
|
||||
|
||||
s.Serve()
|
||||
|
||||
url := testGetServerURL(t, s)
|
||||
@ -114,18 +144,24 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
}()
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode, "using no creds should return unauthorized")
|
||||
|
||||
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
||||
require.NotEmpty(t, wwwAuthHeader, "resp should contain WWW-Authtentication header")
|
||||
require.Contains(t, wwwAuthHeader, fmt.Sprintf("realm=%q", ss.auth.Realm), "WWW-Authtentication header should contain relam")
|
||||
if ss.auth.UserFromHeader == "" {
|
||||
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
||||
require.NotEmpty(t, wwwAuthHeader, "resp should contain WWW-Authtentication header")
|
||||
require.Contains(t, wwwAuthHeader, fmt.Sprintf("realm=%q", ss.auth.Realm), "WWW-Authtentication header should contain relam")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BadCreds", func(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.SetBasicAuth(ss.user+"BAD", ss.pass+"BAD")
|
||||
if ss.user != "" {
|
||||
req.SetBasicAuth(ss.user+"BAD", ss.pass+"BAD")
|
||||
}
|
||||
|
||||
if ss.auth.UserFromHeader != "" {
|
||||
req.Header.Set(ss.auth.UserFromHeader, "/test:")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
@ -134,10 +170,11 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
}()
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode, "using bad creds should return unauthorized")
|
||||
|
||||
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
||||
require.NotEmpty(t, wwwAuthHeader, "resp should contain WWW-Authtentication header")
|
||||
require.Contains(t, wwwAuthHeader, fmt.Sprintf("realm=%q", ss.auth.Realm), "WWW-Authtentication header should contain relam")
|
||||
if ss.auth.UserFromHeader == "" {
|
||||
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
||||
require.NotEmpty(t, wwwAuthHeader, "resp should contain WWW-Authtentication header")
|
||||
require.Contains(t, wwwAuthHeader, fmt.Sprintf("realm=%q", ss.auth.Realm), "WWW-Authtentication header should contain relam")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GoodCreds", func(t *testing.T) {
|
||||
@ -145,7 +182,13 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.SetBasicAuth(ss.user, ss.pass)
|
||||
if ss.user != "" {
|
||||
req.SetBasicAuth(ss.user, ss.pass)
|
||||
}
|
||||
|
||||
if ss.auth.UserFromHeader != "" {
|
||||
req.Header.Set(ss.auth.UserFromHeader, ss.remoteUser)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
@ -155,7 +198,11 @@ func TestMiddlewareAuth(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "using good creds should return ok")
|
||||
|
||||
testExpectRespBody(t, resp, expected)
|
||||
if ss.expectedUser != "" {
|
||||
testExpectRespBody(t, resp, []byte(ss.expectedUser))
|
||||
} else {
|
||||
testExpectRespBody(t, resp, expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -392,16 +392,23 @@ func NewServer(ctx context.Context, options ...Option) (*Server, error) {
|
||||
|
||||
func (s *Server) initAuth() {
|
||||
s.usingAuth = false
|
||||
altUsernameEnabled := s.auth.HtPasswd == "" && s.auth.BasicUser == ""
|
||||
|
||||
authCertificateUserEnabled := s.tlsConfig != nil && s.tlsConfig.ClientAuth != tls.NoClientCert && s.auth.HtPasswd == "" && s.auth.BasicUser == ""
|
||||
if authCertificateUserEnabled {
|
||||
if altUsernameEnabled {
|
||||
s.usingAuth = true
|
||||
s.mux.Use(MiddlewareAuthCertificateUser())
|
||||
if s.auth.UserFromHeader != "" {
|
||||
s.mux.Use(MiddlewareAuthGetUserFromHeader(s.auth.UserFromHeader))
|
||||
} else if s.tlsConfig != nil && s.tlsConfig.ClientAuth != tls.NoClientCert {
|
||||
s.mux.Use(MiddlewareAuthCertificateUser())
|
||||
} else {
|
||||
s.usingAuth = false
|
||||
altUsernameEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
if s.auth.CustomAuthFn != nil {
|
||||
s.usingAuth = true
|
||||
s.mux.Use(MiddlewareAuthCustom(s.auth.CustomAuthFn, s.auth.Realm, authCertificateUserEnabled))
|
||||
s.mux.Use(MiddlewareAuthCustom(s.auth.CustomAuthFn, s.auth.Realm, altUsernameEnabled))
|
||||
return
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user