diff --git a/accounts/go.mod b/accounts/go.mod index 7bc830f6df6..a81f1a02b57 100644 --- a/accounts/go.mod +++ b/accounts/go.mod @@ -36,6 +36,5 @@ require ( replace ( github.com/owncloud/ocis/ocis-pkg => ../ocis-pkg github.com/owncloud/ocis/settings => ../settings - github.com/owncloud/ocis/storage => ../storage google.golang.org/grpc => google.golang.org/grpc v1.26.0 ) diff --git a/changelog/unreleased/user-agent-challenge-lock-in.md b/changelog/unreleased/user-agent-challenge-lock-in.md new file mode 100644 index 00000000000..f89ab4eb2a9 --- /dev/null +++ b/changelog/unreleased/user-agent-challenge-lock-in.md @@ -0,0 +1,20 @@ +Enhancement: Add www-authenticate based on user agent + +Tags: reva, proxy + +We now comply with HTTP spec by adding Www-Authenticate headers on every `401` request. Furthermore, we not only take care of such a thing at the Proxy but also Reva will take care of it. In addition, we now are able to lock-in a set of User-Agent to specific challenges. + +Admins can use this feature by configuring OCIS + Reva following this approach: + +``` +STORAGE_FRONTEND_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT="mirall:basic, Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0:bearer" \ +PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT="mirall:basic, Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0:bearer" \ +PROXY_ENABLE_BASIC_AUTH=true \ +go run cmd/ocis/main.go server +``` + +We introduced two new environment variables: + +`STORAGE_FRONTEND_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT` as well as `PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT`, The reason they have the same value is not to rely on the os env on a distributed environment, so in redundancy we trust. They both configure the same on the backend storage and OCIS Proxy. + +https://github.com/owncloud/ocis/pull/1009 diff --git a/ocis-pkg/conversions/strings.go b/ocis-pkg/conversions/strings.go index a26a5574e50..b712b321be9 100644 --- a/ocis-pkg/conversions/strings.go +++ b/ocis-pkg/conversions/strings.go @@ -1,6 +1,9 @@ package conversions -import "strings" +import ( + "strings" + "unicode/utf8" +) // StringToSliceString splits a string into a slice string according to separator func StringToSliceString(src string, sep string) []string { @@ -12,3 +15,15 @@ func StringToSliceString(src string, sep string) []string { return parts } + +// Reverse reverses a string +func Reverse(s string) string { + size := len(s) + buf := make([]byte, size) + for start := 0; start < size; { + r, n := utf8.DecodeRuneInString(s[start:]) + start += n + utf8.EncodeRune(buf[size-start:], r) + } + return string(buf) +} diff --git a/ocis-pkg/go.sum b/ocis-pkg/go.sum index 9384587a325..fe890f8a699 100644 --- a/ocis-pkg/go.sum +++ b/ocis-pkg/go.sum @@ -118,6 +118,7 @@ github.com/aws/aws-sdk-go v1.34.12 h1:7UbBEYDUa4uW0YmRnOd806MS1yoJMcaodBWDzvBShA github.com/aws/aws-sdk-go v1.34.12/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.35.9 h1:b1HiUpdkFLJyoOQ7zas36YHzjNHH0ivHx/G5lWBeg+U= github.com/aws/aws-sdk-go v1.35.9/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= +github.com/aws/aws-sdk-go v1.35.23 h1:SCP0d0XvyJTDmfnHEQPvBaYi3kea1VNUo7uQmkVgFts= github.com/aws/aws-sdk-go v1.35.23/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go v1.35.27/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04= @@ -208,6 +209,7 @@ github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666 h1:E7VsSSN/2YZLS github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00 h1:LVl25JaflluOchVvaHWtoCynm5OaM+VNai0IYkcCSe0= github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y= github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/reva v1.1.0 h1:Gih6ECHvMMGSx523SFluFlDmNMuhYelXYShdWvjvW38= github.com/cs3org/reva v1.1.0/go.mod h1:fBzTrNuAKdQ62ybjpdu8nyhBin90/3/3s6DGQDCdBp4= @@ -323,6 +325,7 @@ github.com/go-ozzo/ozzo-validation/v4 v4.2.1 h1:XALUNshPYumA7UShB7iM3ZVlqIBn0jfw github.com/go-ozzo/ozzo-validation/v4 v4.2.1/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= @@ -953,6 +956,7 @@ github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCv github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pkg/xattr v0.4.1 h1:dhclzL6EqOXNaPDWqoeb9tIxATfBSmjqL0b4DpSjwRw= github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs= +github.com/pkg/xattr v0.4.2 h1:fbVxr9lvkToTGgPljVszvFsOdcbSv5BmGABneyxRgZM= github.com/pkg/xattr v0.4.2/go.mod h1:sBD3RAqlr8Q+RC3FutZcikpT8nyDrIEEBw2J744gVWs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1410,6 +1414,7 @@ golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/ocis/go.mod b/ocis/go.mod index 01ca7ada1ac..a49d62b5c89 100644 --- a/ocis/go.mod +++ b/ocis/go.mod @@ -7,6 +7,7 @@ require ( contrib.go.opencensus.io/exporter/ocagent v0.7.0 contrib.go.opencensus.io/exporter/zipkin v0.1.2 github.com/UnnoTed/fileb0x v1.1.4 + github.com/cs3org/reva v1.4.1-0.20201203075131-783e35cbff51 // indirect github.com/go-test/deep v1.0.6 // indirect github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/micro/cli/v2 v2.1.2 diff --git a/ocis/pkg/command/storagefrontend.go b/ocis/pkg/command/storagefrontend.go index 5b7296d2794..8f56ad64c43 100644 --- a/ocis/pkg/command/storagefrontend.go +++ b/ocis/pkg/command/storagefrontend.go @@ -21,6 +21,10 @@ func StorageFrontendCommand(cfg *config.Config) *cli.Command { Action: func(c *cli.Context) error { scfg := configureStorageFrontend(cfg) + if err := command.Frontend(scfg).Before(c); err != nil { + return err + } + return cli.HandleAction( command.Frontend(scfg).Action, c, diff --git a/proxy/go.sum b/proxy/go.sum index 5adc8c88b2d..751f9553603 100644 --- a/proxy/go.sum +++ b/proxy/go.sum @@ -192,6 +192,7 @@ github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666 h1:E7VsSSN/2YZLS github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00 h1:LVl25JaflluOchVvaHWtoCynm5OaM+VNai0IYkcCSe0= github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y= github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/reva v1.2.2-0.20200924071957-e6676516e61e/go.mod h1:DOV5SjpOBKN+aWfOHLdA4KiLQkpyC786PQaXEdRAZ0M= github.com/cs3org/reva v1.4.1-0.20201120104232-f5afafc04c3b h1:bDGaeyhTFrdLF3Pm8XdJ60ADrE4f+f/Mz2hkICvQHJM= diff --git a/proxy/pkg/command/server.go b/proxy/pkg/command/server.go index dad51e24971..a29de79d742 100644 --- a/proxy/pkg/command/server.go +++ b/proxy/pkg/command/server.go @@ -3,6 +3,7 @@ package command import ( "context" "crypto/tls" + "fmt" "net/http" "os" "os/signal" @@ -19,6 +20,7 @@ import ( openzipkin "github.com/openzipkin/zipkin-go" zipkinhttp "github.com/openzipkin/zipkin-go/reporter/http" acc "github.com/owncloud/ocis/accounts/pkg/proto/v0" + "github.com/owncloud/ocis/ocis-pkg/conversions" "github.com/owncloud/ocis/ocis-pkg/log" "github.com/owncloud/ocis/ocis-pkg/service/grpc" "github.com/owncloud/ocis/proxy/pkg/config" @@ -48,8 +50,10 @@ func Server(cfg *config.Config) *cli.Command { } cfg.PreSignedURL.AllowedHTTPMethods = ctx.StringSlice("presignedurl-allow-method") - // When running on single binary mode the before hook from the root command won't get called. We manually - // call this before hook from ocis command, so the configuration can be loaded. + if err := loadUserAgent(ctx, cfg); err != nil { + return err + } + return ParseConfig(ctx, cfg) }, Action: func(c *cli.Context) error { @@ -268,8 +272,8 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic return alice.New( middleware.HTTPSRedirect, - middleware.OIDCAuth( - middleware.Logger(l), + middleware.Authentication( + // OIDC Options middleware.OIDCProviderFunc(func() (middleware.OIDCProvider, error) { // Initialize a provider by specifying the issuer URL. // it will fetch the keys from the issuer using the .well-known @@ -280,15 +284,15 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic ) }), middleware.HTTPClient(oidcHTTPClient), - middleware.OIDCIss(cfg.OIDC.Issuer), middleware.TokenCacheSize(cfg.OIDC.UserinfoCache.Size), middleware.TokenCacheTTL(time.Second*time.Duration(cfg.OIDC.UserinfoCache.TTL)), - ), - middleware.BasicAuth( + + // basic Options middleware.Logger(l), middleware.EnableBasicAuth(cfg.EnableBasicAuth), middleware.AccountsClient(accountsClient), middleware.OIDCIss(cfg.OIDC.Issuer), + middleware.CredentialsByUserAgent(cfg.Reva.Middleware.Auth.CredentialsByUserAgent), ), middleware.SignedURLAuth( middleware.Logger(l), @@ -312,3 +316,28 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic ), ) } + +// loadUserAgent reads the proxy-user-agent-lock-in, since it is a string flag, and attempts to construct a map of +// "user-agent":"challenge" locks in for Reva. +// Modifies cfg. Spaces don't need to be trimmed as urfavecli takes care of it. User agents with spaces are valid. i.e: +// Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0 +// This function works by relying in our format of specifying [user-agent:challenge] and the fact that the user agent +// might contain ":" (colon), so the original string is reversed, split in two parts, by the time it is split we +// have the indexes reversed and the tuple is in the format of [challenge:user-agent], then the same process is applied +// in reverse for each individual part +func loadUserAgent(c *cli.Context, cfg *config.Config) error { + cfg.Reva.Middleware.Auth.CredentialsByUserAgent = make(map[string]string) + locks := c.StringSlice("proxy-user-agent-lock-in") + + for _, v := range locks { + vv := conversions.Reverse(v) + parts := strings.SplitN(vv, ":", 2) + if len(parts) != 2 { + return fmt.Errorf("unexpected config value for user-agent lock-in: %v, expected format is user-agent:challenge", v) + } + + cfg.Reva.Middleware.Auth.CredentialsByUserAgent[conversions.Reverse(parts[1])] = conversions.Reverse(parts[0]) + } + + return nil +} diff --git a/proxy/pkg/config/config.go b/proxy/pkg/config/config.go index 4da46e0065a..2f42bbe0be9 100644 --- a/proxy/pkg/config/config.go +++ b/proxy/pkg/config/config.go @@ -80,7 +80,18 @@ var ( // Reva defines all available REVA configuration. type Reva struct { - Address string + Address string + Middleware Middleware +} + +// Middleware configures reva middlewares. +type Middleware struct { + Auth Auth +} + +// Auth configures reva http auth middleware. +type Auth struct { + CredentialsByUserAgent map[string]string } // Cache is a TTL cache configuration. diff --git a/proxy/pkg/flagset/flagset.go b/proxy/pkg/flagset/flagset.go index b8e3fb870f4..e76373dc0d9 100644 --- a/proxy/pkg/flagset/flagset.go +++ b/proxy/pkg/flagset/flagset.go @@ -232,7 +232,7 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { Destination: &cfg.AutoprovisionAccounts, }, - // Presigned URLs + // Pre Signed URLs &cli.StringSliceFlag{ Name: "presignedurl-allow-method", Value: cli.NewStringSlice("GET"), @@ -255,8 +255,14 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { EnvVars: []string{"PROXY_ENABLE_BASIC_AUTH"}, Destination: &cfg.EnableBasicAuth, }, - } + // Reva Middlewares Config + &cli.StringSliceFlag{ + Name: "proxy-user-agent-lock-in", + Usage: "--user-agent-whitelist-lock-in=mirall:basic,foo:bearer Given a tuple of [UserAgent:challenge] it locks a given user agent to the authentication challenge. Particularly useful for old clients whose USer-Agent is known and only support one authentication challenge. When this flag is set in the proxy it configures the authentication middlewares.", + EnvVars: []string{"PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT"}, + }, + } } // ListProxyWithConfig applies the config to the list commands flags. diff --git a/proxy/pkg/middleware/authentication.go b/proxy/pkg/middleware/authentication.go new file mode 100644 index 00000000000..2600ab3c441 --- /dev/null +++ b/proxy/pkg/middleware/authentication.go @@ -0,0 +1,131 @@ +package middleware + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "time" +) + +var ( + // SupportedAuthStrategies stores configured challenges. + SupportedAuthStrategies []string + + // ProxyWwwAuthenticate is a list of endpoints that do not rely on reva underlying authentication, such as ocs. + // services that fallback to reva authentication are declared in the "frontend" command on OCIS. It is a list of strings + // to be regexp compiled. + ProxyWwwAuthenticate = []string{"/ocs/v[12].php/cloud/"} + + // WWWAuthenticate captures the Www-Authenticate header string. + WWWAuthenticate = "Www-Authenticate" +) + +// userAgentLocker aids in dependency injection for helper methods. The set of fields is arbitrary and the only relation +// they share is to fulfill their duty and lock a User-Agent to its correct challenge if configured. +type userAgentLocker struct { + w http.ResponseWriter + r *http.Request + locks map[string]string // locks represents a reva user-agent:challenge mapping. + fallback string +} + +// Authentication is a higher order authentication middleware. +func Authentication(opts ...Option) func(next http.Handler) http.Handler { + options := newOptions(opts...) + + configureSupportedChallenges(options) + oidc := newOIDCAuth(options) + basic := newBasicAuth(options) + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if options.OIDCIss != "" && options.EnableBasicAuth { + oidc(basic(next)).ServeHTTP(w, r) + } + + if options.OIDCIss != "" && !options.EnableBasicAuth { + oidc(next).ServeHTTP(w, r) + } + + if options.OIDCIss == "" && options.EnableBasicAuth { + basic(next).ServeHTTP(w, r) + } + }) + } +} + +// configureSupportedChallenges adds known authentication challenges to the current session. +func configureSupportedChallenges(options Options) { + if options.OIDCIss != "" { + SupportedAuthStrategies = append(SupportedAuthStrategies, "bearer") + } + + if options.EnableBasicAuth { + SupportedAuthStrategies = append(SupportedAuthStrategies, "basic") + } +} + +func writeSupportedAuthenticateHeader(w http.ResponseWriter, r *http.Request) { + for i := 0; i < len(SupportedAuthStrategies); i++ { + w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(SupportedAuthStrategies[i]), r.Host)) + } +} + +func removeSuperfluousAuthenticate(w http.ResponseWriter) { + w.Header().Del(WWWAuthenticate) +} + +// userAgentAuthenticateLockIn sets Www-Authenticate according to configured user agents. This is useful for the case of +// legacy clients that do not support protocols like OIDC or OAuth and want to lock a given user agent to a challenge +// such as basic. For more context check https://github.com/cs3org/reva/pull/1350 +func userAgentAuthenticateLockIn(w http.ResponseWriter, r *http.Request, locks map[string]string, fallback string) { + u := userAgentLocker{ + w: w, + r: r, + locks: locks, + fallback: fallback, + } + + for i := 0; i < len(ProxyWwwAuthenticate); i++ { + evalRequestURI(&u, i) + } +} + +func evalRequestURI(l *userAgentLocker, i int) { + r := regexp.MustCompile(ProxyWwwAuthenticate[i]) + if r.Match([]byte(l.r.RequestURI)) { + for k, v := range l.locks { + if strings.Contains(k, l.r.UserAgent()) { + removeSuperfluousAuthenticate(l.w) + l.w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), l.r.Host)) + return + } + } + l.w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(l.fallback), l.r.Host)) + } +} + +// newOIDCAuth returns a configured oidc middleware +func newOIDCAuth(options Options) func(http.Handler) http.Handler { + return OIDCAuth( + Logger(options.Logger), + OIDCProviderFunc(options.OIDCProviderFunc), + HTTPClient(options.HTTPClient), + OIDCIss(options.OIDCIss), + TokenCacheSize(options.UserinfoCacheSize), + TokenCacheTTL(time.Second*time.Duration(options.UserinfoCacheTTL)), + CredentialsByUserAgent(options.CredentialsByUserAgent), + ) +} + +// newBasicAuth returns a configured basic middleware +func newBasicAuth(options Options) func(http.Handler) http.Handler { + return BasicAuth( + Logger(options.Logger), + EnableBasicAuth(options.EnableBasicAuth), + AccountsClient(options.AccountsClient), + OIDCIss(options.OIDCIss), + CredentialsByUserAgent(options.CredentialsByUserAgent), + ) +} diff --git a/proxy/pkg/middleware/basic_auth.go b/proxy/pkg/middleware/basic_auth.go index f06879b012d..c16eb8defbc 100644 --- a/proxy/pkg/middleware/basic_auth.go +++ b/proxy/pkg/middleware/basic_auth.go @@ -2,11 +2,12 @@ package middleware import ( "fmt" + "net/http" + "strings" + accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0" "github.com/owncloud/ocis/ocis-pkg/log" "github.com/owncloud/ocis/ocis-pkg/oidc" - "net/http" - "strings" ) const publicFilesEndpoint = "/remote.php/dav/public-files/" @@ -31,13 +32,36 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, req *http.Request) { if h.isPublicLink(req) || !h.isBasicAuth(req) { + if !h.isPublicLink(req) { + userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "basic") + } next.ServeHTTP(w, req) return } + removeSuperfluousAuthenticate(w) account, ok := h.getAccount(req) + // touch is a user agent locking guard, when touched changes to true it indicates the User-Agent on the + // request is configured to support only one challenge, it it remains untouched, there are no considera- + // tions and we should write all available authentication challenges to the response. + touch := false + if !ok { + for k, v := range options.CredentialsByUserAgent { + if strings.Contains(k, req.UserAgent()) { + removeSuperfluousAuthenticate(w) + w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host)) + touch = true + break + } + } + + // if the request is not bound to any user agent, write all available challenges + if !touch { + writeSupportedAuthenticateHeader(w, req) + } + w.WriteHeader(http.StatusUnauthorized) return } diff --git a/proxy/pkg/middleware/oidc_auth.go b/proxy/pkg/middleware/oidc_auth.go index ace5a045b45..098ff8d079e 100644 --- a/proxy/pkg/middleware/oidc_auth.go +++ b/proxy/pkg/middleware/oidc_auth.go @@ -37,7 +37,10 @@ func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // there is no bearer token on the request, if !h.shouldServe(req) { + // oidc supported but token not present, add header and handover to the next middleware. + userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "bearer") next.ServeHTTP(w, req) return } diff --git a/proxy/pkg/middleware/options.go b/proxy/pkg/middleware/options.go index 9d5d0c1ca96..27906f9a5c3 100644 --- a/proxy/pkg/middleware/options.go +++ b/proxy/pkg/middleware/options.go @@ -46,6 +46,8 @@ type Options struct { UserinfoCacheSize int // UserinfoCacheTTL sets the max cache duration for the userinfo cache, intended for the oidc_auth middleware UserinfoCacheTTL time.Duration + // CredentialsByUserAgent sets the auth challenges on a per user-agent basis + CredentialsByUserAgent map[string]string } // newOptions initializes the available default options. @@ -108,6 +110,13 @@ func OIDCIss(iss string) Option { } } +// CredentialsByUserAgent sets UserAgentChallenges. +func CredentialsByUserAgent(v map[string]string) Option { + return func(o *Options) { + o.CredentialsByUserAgent = v + } +} + // RevaGatewayClient provides a function to set the the reva gateway service client option. func RevaGatewayClient(gc gateway.GatewayAPIClient) Option { return func(o *Options) { diff --git a/storage/go.sum b/storage/go.sum index f1c02114e69..612d43817a5 100644 --- a/storage/go.sum +++ b/storage/go.sum @@ -196,6 +196,7 @@ github.com/cs3org/go-cs3apis v0.0.0-20200730121022-c4f3d4f7ddfd/go.mod h1:UXha4T github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00 h1:LVl25JaflluOchVvaHWtoCynm5OaM+VNai0IYkcCSe0= github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y= github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/reva v1.1.0/go.mod h1:fBzTrNuAKdQ62ybjpdu8nyhBin90/3/3s6DGQDCdBp4= github.com/cs3org/reva v1.2.2-0.20200924071957-e6676516e61e/go.mod h1:DOV5SjpOBKN+aWfOHLdA4KiLQkpyC786PQaXEdRAZ0M= diff --git a/storage/pkg/command/frontend.go b/storage/pkg/command/frontend.go index 9a1bed16412..fa41ee7fab9 100644 --- a/storage/pkg/command/frontend.go +++ b/storage/pkg/command/frontend.go @@ -6,12 +6,14 @@ import ( "os" "os/signal" "path" + "strings" "time" "github.com/cs3org/reva/cmd/revad/runtime" "github.com/gofrs/uuid" "github.com/micro/cli/v2" "github.com/oklog/run" + "github.com/owncloud/ocis/ocis-pkg/conversions" "github.com/owncloud/ocis/storage/pkg/config" "github.com/owncloud/ocis/storage/pkg/flagset" "github.com/owncloud/ocis/storage/pkg/server/debug" @@ -25,8 +27,7 @@ func Frontend(cfg *config.Config) *cli.Command { Flags: flagset.FrontendWithConfig(cfg), Before: func(c *cli.Context) error { cfg.Reva.Frontend.Services = c.StringSlice("service") - - return nil + return loadUserAgent(c, cfg) }, Action: func(c *cli.Context) error { logger := NewLogger(cfg) @@ -115,6 +116,9 @@ func Frontend(cfg *config.Config) *cli.Command { "cors": map[string]interface{}{ "allow_credentials": true, }, + "auth": map[string]interface{}{ + "credentials_by_user_agent": cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent, + }, }, // TODO build services dynamically "services": map[string]interface{}{ @@ -298,3 +302,28 @@ func Frontend(cfg *config.Config) *cli.Command { }, } } + +// loadUserAgent reads the user-agent-whitelist-lock-in, since it is a string flag, and attempts to construct a map of +// "user-agent":"challenge" locks in for Reva. +// Modifies cfg. Spaces don't need to be trimmed as urfavecli takes care of it. User agents with spaces are valid. i.e: +// Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0 +// This function works by relying in our format of specifying [user-agent:challenge] and the fact that the user agent +// might contain ":" (colon), so the original string is reversed, split in two parts, by the time it is split we +// have the indexes reversed and the tuple is in the format of [challenge:user-agent], then the same process is applied +// in reverse for each individual part +func loadUserAgent(c *cli.Context, cfg *config.Config) error { + cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent = make(map[string]string) + locks := c.StringSlice("user-agent-whitelist-lock-in") + + for _, v := range locks { + vv := conversions.Reverse(v) + parts := strings.SplitN(vv, ":", 2) + if len(parts) != 2 { + return fmt.Errorf("unexpected config value for user-agent lock-in: %v, expected format is user-agent:challenge", v) + } + + cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent[conversions.Reverse(parts[1])] = conversions.Reverse(parts[0]) + } + + return nil +} diff --git a/storage/pkg/config/config.go b/storage/pkg/config/config.go index ebf46302de5..d60a166fa26 100644 --- a/storage/pkg/config/config.go +++ b/storage/pkg/config/config.go @@ -84,6 +84,17 @@ type FrontendPort struct { OCSPrefix string OCSSharePrefix string PublicURL string + Middleware Middleware +} + +// Middleware configures reva middlewares. +type Middleware struct { + Auth Auth +} + +// Auth configures reva http auth middleware. +type Auth struct { + CredentialsByUserAgent map[string]string } // DataGatewayPort has a public url diff --git a/storage/pkg/flagset/frontend.go b/storage/pkg/flagset/frontend.go index 7b132a94f72..cddbc5e5dc2 100644 --- a/storage/pkg/flagset/frontend.go +++ b/storage/pkg/flagset/frontend.go @@ -139,6 +139,13 @@ func FrontendWithConfig(cfg *config.Config) []cli.Flag { EnvVars: []string{"STORAGE_FRONTEND_UPLOAD_HTTP_METHOD_OVERRIDE"}, Destination: &cfg.Reva.UploadHTTPMethodOverride, }, + + // Reva Middlewares Config + &cli.StringSliceFlag{ + Name: "user-agent-whitelist-lock-in", + Usage: "--user-agent-whitelist-lock-in=mirall:basic,foo:bearer Given a tuple of comma separated [UserAgent:challenge] values, it locks a given user agent to the authentication challenge. Particularly useful for old clients whose USer-Agent is known and only support one authentication challenge. When this flag is set in the storage-frontend it configures Reva.", + EnvVars: []string{"STORAGE_FRONTEND_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT"}, + }, } flags = append(flags, TracingWithConfig(cfg)...)