mirror of
https://github.com/rclone/rclone.git
synced 2025-02-12 16:40:14 +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
|
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.
|
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
|
certificates are required by the ` + "`--client-ca`" + ` flag passed to the server, the
|
||||||
client certificate common name will be considered as the username.
|
client certificate common name will be considered as the username.
|
||||||
|
|
||||||
@ -85,16 +89,21 @@ var AuthConfigInfo = fs.Options{{
|
|||||||
Name: "salt",
|
Name: "salt",
|
||||||
Default: "dlPL2MqE",
|
Default: "dlPL2MqE",
|
||||||
Help: "Password hashing salt",
|
Help: "Password hashing salt",
|
||||||
|
}, {
|
||||||
|
Name: "user_from_header",
|
||||||
|
Default: "",
|
||||||
|
Help: "User name from a defined HTTP header",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// AuthConfig contains options for the http authentication
|
// AuthConfig contains options for the http authentication
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
HtPasswd string `config:"htpasswd"` // htpasswd file - if not provided no authentication is done
|
HtPasswd string `config:"htpasswd"` // htpasswd file - if not provided no authentication is done
|
||||||
Realm string `config:"realm"` // realm for authentication
|
Realm string `config:"realm"` // realm for authentication
|
||||||
BasicUser string `config:"user"` // single username for basic auth if not using Htpasswd
|
BasicUser string `config:"user"` // single username for basic auth if not using Htpasswd
|
||||||
BasicPass string `config:"pass"` // password for BasicUser
|
BasicPass string `config:"pass"` // password for BasicUser
|
||||||
Salt string `config:"salt"` // password hashing salt
|
Salt string `config:"salt"` // password hashing salt
|
||||||
CustomAuthFn CustomAuthFn `json:"-" config:"-"` // custom Auth (not set by command line flags)
|
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
|
// 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.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.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.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
|
// AddAuthFlagsPrefix adds flags to the flag set for AuthConfig
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"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
|
var onlyOnceWarningAllowOrigin sync.Once
|
||||||
|
|
||||||
// MiddlewareCORS instantiates middleware that handles basic CORS protections for rcd
|
// MiddlewareCORS instantiates middleware that handles basic CORS protections for rcd
|
||||||
|
@ -14,11 +14,13 @@ import (
|
|||||||
|
|
||||||
func TestMiddlewareAuth(t *testing.T) {
|
func TestMiddlewareAuth(t *testing.T) {
|
||||||
servers := []struct {
|
servers := []struct {
|
||||||
name string
|
name string
|
||||||
http Config
|
expectedUser string
|
||||||
auth AuthConfig
|
remoteUser string
|
||||||
user string
|
http Config
|
||||||
pass string
|
auth AuthConfig
|
||||||
|
user string
|
||||||
|
pass string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Basic",
|
name: "Basic",
|
||||||
@ -85,9 +87,32 @@ func TestMiddlewareAuth(t *testing.T) {
|
|||||||
},
|
},
|
||||||
user: "custom",
|
user: "custom",
|
||||||
pass: "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 {
|
for _, ss := range servers {
|
||||||
t.Run(ss.name, func(t *testing.T) {
|
t.Run(ss.name, func(t *testing.T) {
|
||||||
s, err := NewServer(context.Background(), WithConfig(ss.http), WithAuth(ss.auth))
|
s, err := NewServer(context.Background(), WithConfig(ss.http), WithAuth(ss.auth))
|
||||||
@ -97,7 +122,12 @@ func TestMiddlewareAuth(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
expected := []byte("secret-page")
|
expected := []byte("secret-page")
|
||||||
s.Router().Mount("/", testEchoHandler(expected))
|
if ss.expectedUser != "" {
|
||||||
|
s.Router().Mount("/", testAuthUserHandler())
|
||||||
|
} else {
|
||||||
|
s.Router().Mount("/", testEchoHandler(expected))
|
||||||
|
}
|
||||||
|
|
||||||
s.Serve()
|
s.Serve()
|
||||||
|
|
||||||
url := testGetServerURL(t, s)
|
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")
|
require.Equal(t, http.StatusUnauthorized, resp.StatusCode, "using no creds should return unauthorized")
|
||||||
|
if ss.auth.UserFromHeader == "" {
|
||||||
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
||||||
require.NotEmpty(t, wwwAuthHeader, "resp should contain WWW-Authtentication header")
|
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")
|
require.Contains(t, wwwAuthHeader, fmt.Sprintf("realm=%q", ss.auth.Realm), "WWW-Authtentication header should contain relam")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("BadCreds", func(t *testing.T) {
|
t.Run("BadCreds", func(t *testing.T) {
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
require.NoError(t, err)
|
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)
|
resp, err := client.Do(req)
|
||||||
require.NoError(t, err)
|
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")
|
require.Equal(t, http.StatusUnauthorized, resp.StatusCode, "using bad creds should return unauthorized")
|
||||||
|
if ss.auth.UserFromHeader == "" {
|
||||||
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
wwwAuthHeader := resp.Header.Get("WWW-Authenticate")
|
||||||
require.NotEmpty(t, wwwAuthHeader, "resp should contain WWW-Authtentication header")
|
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")
|
require.Contains(t, wwwAuthHeader, fmt.Sprintf("realm=%q", ss.auth.Realm), "WWW-Authtentication header should contain relam")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("GoodCreds", func(t *testing.T) {
|
t.Run("GoodCreds", func(t *testing.T) {
|
||||||
@ -145,7 +182,13 @@ func TestMiddlewareAuth(t *testing.T) {
|
|||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
require.NoError(t, err)
|
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)
|
resp, err := client.Do(req)
|
||||||
require.NoError(t, err)
|
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")
|
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() {
|
func (s *Server) initAuth() {
|
||||||
s.usingAuth = false
|
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 altUsernameEnabled {
|
||||||
if authCertificateUserEnabled {
|
|
||||||
s.usingAuth = true
|
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 {
|
if s.auth.CustomAuthFn != nil {
|
||||||
s.usingAuth = true
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user