From b20b05806ea590bbecf3b4d54d25bae0886ece0e Mon Sep 17 00:00:00 2001 From: Ilja Neumann Date: Wed, 25 Mar 2020 21:17:20 +0100 Subject: [PATCH] Integrate oid-middleware This feature is required for user-based routing. --- changelog/unreleased/add-oidc.md | 8 +++ config/proxy-example-oidc.json | 107 +++++++++++++++++++++++++++++ go.mod | 3 +- pkg/command/server.go | 44 +++++++++--- pkg/config/config.go | 10 +++ pkg/middleware/openidconnect.go | 114 +++++++++++++++++++++++++++++++ pkg/server/http/option.go | 22 ++++-- pkg/server/http/server.go | 16 ++++- 8 files changed, 305 insertions(+), 19 deletions(-) create mode 100644 changelog/unreleased/add-oidc.md create mode 100644 config/proxy-example-oidc.json create mode 100644 pkg/middleware/openidconnect.go diff --git a/changelog/unreleased/add-oidc.md b/changelog/unreleased/add-oidc.md new file mode 100644 index 0000000000..d1a9684f6c --- /dev/null +++ b/changelog/unreleased/add-oidc.md @@ -0,0 +1,8 @@ +Enhancement: Configurable OpenID Connect client + +The proxy will try to authenticate every request with the configured OIDC provider. + +See configs/proxy-example.oidc.json for an example-configuration. + +https://github.com/owncloud/ocis-proxy/pull/27 + diff --git a/config/proxy-example-oidc.json b/config/proxy-example-oidc.json new file mode 100644 index 0000000000..a174894580 --- /dev/null +++ b/config/proxy-example-oidc.json @@ -0,0 +1,107 @@ +{ + "HTTP": { + "Namespace": "com.owncloud" + }, + "oidc": { + "endpoint": "https://localhost:9200", + "realm": "", + "signing_algs": ["RS256", "PS256"], + "insecure": true + }, + "policies": [ + { + "name": "reva", + "routes": [ + { + "endpoint": "/", + "backend": "http://localhost:9100" + }, + { + "endpoint": "/.well-known/", + "backend": "http://localhost:9130" + }, + { + "endpoint": "/konnect/", + "backend": "http://localhost:9130" + }, + { + "endpoint": "/signin/", + "backend": "http://localhost:9130" + }, + { + "endpoint": "/ocs/", + "backend": "http://localhost:9140" + }, + { + "endpoint": "/remote.php/", + "backend": "http://localhost:9140" + }, + { + "endpoint": "/dav/", + "backend": "http://localhost:9140" + }, + { + "endpoint": "/webdav/", + "backend": "http://localhost:9140" + }, + { + "endpoint": "/status.php", + "backend": "http://localhost:9140" + }, + { + "endpoint": "/index.php/", + "backend": "http://localhost:9140" + } + ] + }, + { + "name": "oc10", + "routes": [ + { + "endpoint": "/", + "backend": "http://localhost:9100" + }, + { + "endpoint": "/.well-known/", + "backend": "http://localhost:9130" + }, + { + "endpoint": "/konnect/", + "backend": "http://localhost:9130" + }, + { + "endpoint": "/signin/", + "backend": "http://localhost:9130" + }, + { + "endpoint": "/ocs/", + "backend": "https://demo.owncloud.com", + "apache-vhost": true + }, + { + "endpoint": "/remote.php/", + "backend": "https://demo.owncloud.com", + "apache-vhost": true + }, + { + "endpoint": "/dav/", + "backend": "https://demo.owncloud.com", + "apache-vhost": true + }, + { + "endpoint": "/webdav/", + "backend": "https://demo.owncloud.com", + "apache-vhost": true + }, + { + "endpoint": "/status.php", + "backend": "https://demo.owncloud.com" + }, + { + "endpoint": "/index.php/", + "backend": "https://demo.owncloud.com" + } + ] + } + ] +} diff --git a/go.mod b/go.mod index 8c6d85ba10..aa601ed278 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( contrib.go.opencensus.io/exporter/jaeger v0.2.0 contrib.go.opencensus.io/exporter/ocagent v0.6.0 contrib.go.opencensus.io/exporter/zipkin v0.1.1 + github.com/coreos/go-oidc v2.1.0+incompatible github.com/micro/cli/v2 v2.1.2-0.20200203150404-894195727d9c github.com/micro/go-micro/v2 v2.0.1-0.20200212105717-d76baf59de2e // indirect github.com/oklog/run v1.1.0 @@ -18,7 +19,7 @@ require ( github.com/spf13/viper v1.6.2 go.opencensus.io v0.22.2 golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect - golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect + golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect gopkg.in/square/go-jose.v2 v2.4.0 // indirect diff --git a/pkg/command/server.go b/pkg/command/server.go index a40f146dbc..3ea351d0c4 100644 --- a/pkg/command/server.go +++ b/pkg/command/server.go @@ -2,6 +2,10 @@ package command import ( "context" + "github.com/owncloud/ocis-pkg/v2/log" + "github.com/owncloud/ocis-pkg/v2/oidc" + "github.com/owncloud/ocis-proxy/pkg/middleware" + "net/http" "os" "os/signal" "strings" @@ -19,7 +23,7 @@ import ( "github.com/owncloud/ocis-proxy/pkg/metrics" "github.com/owncloud/ocis-proxy/pkg/proxy" "github.com/owncloud/ocis-proxy/pkg/server/debug" - "github.com/owncloud/ocis-proxy/pkg/server/http" + proxyHTTP "github.com/owncloud/ocis-proxy/pkg/server/http" "go.opencensus.io/stats/view" "go.opencensus.io/trace" ) @@ -141,15 +145,16 @@ func Server(cfg *config.Config) *cli.Command { ) { - server, err := http.Server( - http.Handler(rp), - http.Logger(logger), - http.Namespace(httpNamespace), - http.Context(ctx), - http.Config(cfg), - http.Metrics(metrics), - http.Flags(flagset.RootWithConfig(config.New())), - http.Flags(flagset.ServerWithConfig(config.New())), + server, err := proxyHTTP.Server( + proxyHTTP.Handler(rp), + proxyHTTP.Logger(logger), + proxyHTTP.Namespace(httpNamespace), + proxyHTTP.Context(ctx), + proxyHTTP.Config(cfg), + proxyHTTP.Metrics(metrics), + proxyHTTP.Flags(flagset.RootWithConfig(config.New())), + proxyHTTP.Flags(flagset.ServerWithConfig(config.New())), + proxyHTTP.Middlewares(loadMiddlewares(cfg, logger)...), ) if err != nil { @@ -228,3 +233,22 @@ func Server(cfg *config.Config) *cli.Command { }, } } + +func loadMiddlewares(cfg *config.Config, l log.Logger) []func(handler http.Handler) http.Handler { + var configuredMiddlewares = make([]func(handler http.Handler) http.Handler, 0) + if cfg.OIDC != nil { + l.Info().Msg("Loading OIDC-Middleware") + l.Debug().Interface("oidc_config", cfg.OIDC).Msg("OIDC-Config") + oidcMW := middleware.OpenIDConnect( + oidc.Endpoint(cfg.OIDC.Endpoint), + oidc.Insecure(cfg.OIDC.Insecure), + oidc.Realm(cfg.OIDC.Realm), + oidc.SigningAlgs(cfg.OIDC.SigningAlgs), + oidc.Logger(l), + ) + + configuredMiddlewares = append(configuredMiddlewares, oidcMW) + } + + return configuredMiddlewares +} diff --git a/pkg/config/config.go b/pkg/config/config.go index b0738a4e1a..6750fb716c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -80,6 +80,16 @@ type Config struct { Tracing Tracing Asset Asset Policies []Policy + OIDC *OIDC +} + +// OIDC is the config for the OpenID-Connect middleware. If set the proxy will try to authenticate every request +// with the configured oidc-provider +type OIDC struct { + Endpoint string + Realm string + SigningAlgs []string + Insecure bool } // New initializes a new configuration diff --git a/pkg/middleware/openidconnect.go b/pkg/middleware/openidconnect.go new file mode 100644 index 0000000000..0e6d0dfbe9 --- /dev/null +++ b/pkg/middleware/openidconnect.go @@ -0,0 +1,114 @@ +package middleware + +import ( + "context" + "crypto/tls" + "errors" + "net/http" + "strings" + "time" + + oidc "github.com/coreos/go-oidc" + ocisoidc "github.com/owncloud/ocis-pkg/v2/oidc" + "golang.org/x/oauth2" +) + +var ( + // ErrInvalidToken is returned when the request token is invalid. + ErrInvalidToken = errors.New("invalid or missing token") +) + +// newOIDCOptions initializes the available default options. +func newOIDCOptions(opts ...ocisoidc.Option) ocisoidc.Options { + opt := ocisoidc.Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// OpenIDConnect provides a middleware to check access secured by a static token. +func OpenIDConnect(opts ...ocisoidc.Option) func(http.Handler) http.Handler { + opt := newOIDCOptions(opts...) + + // set defaults + if opt.Realm == "" { + opt.Realm = opt.Endpoint + } + if len(opt.SigningAlgs) < 1 { + opt.SigningAlgs = []string{"RS256", "PS256"} + } + + var oidcProvider *oidc.Provider + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + header := r.Header.Get("Authorization") + path := r.URL.Path + + // Ignore request to "/konnect/v1/userinfo" as this will cause endless loop when getting userinfo + // needs a better idea on how to not hardcode this + if header == "" || !strings.HasPrefix(header, "Bearer ") || path == "/konnect/v1/userinfo" { + next.ServeHTTP(w, r) + return + } + + token := header[7:] + customHTTPClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: opt.Insecure, + }, + }, + Timeout: time.Second * 10, + } + + customCtx := context.WithValue(r.Context(), oauth2.HTTPClient, customHTTPClient) + + // use cached provider + if oidcProvider == nil { + // Initialize a provider by specifying the issuer URL. + // provider needs to be cached as when it is created + // it will fetch the keys from the issuer using the .well-known + // endpoint + provider, err := oidc.NewProvider(customCtx, opt.Endpoint) + if err != nil { + opt.Logger.Error().Err(err).Msg("could not initialize oidc provider") + w.WriteHeader(http.StatusInternalServerError) + return + } + oidcProvider = provider + } + + // The claims we want to have + var claims ocisoidc.StandardClaims + + // TODO cache userinfo for access token if we can determine the expiry (which works in case it is a jwt based access token) + oauth2Token := &oauth2.Token{ + AccessToken: token, + } + userInfo, err := oidcProvider.UserInfo(customCtx, oauth2.StaticTokenSource(oauth2Token)) + if err != nil { + opt.Logger.Error().Err(err).Str("token", token).Msg("Failed to get userinfo") + http.Error(w, ErrInvalidToken.Error(), http.StatusUnauthorized) + return + } + + // parse claims + if err := userInfo.Claims(&claims); err != nil { + opt.Logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims") + w.WriteHeader(http.StatusInternalServerError) + return + } + + opt.Logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Msg("unmarshalled userinfo") + // store claims in context + // uses the original context, not the one with probably reduced security + nr := r.WithContext(ocisoidc.NewContext(r.Context(), &claims)) + + next.ServeHTTP(w, nr) + }) + } +} diff --git a/pkg/server/http/option.go b/pkg/server/http/option.go index b665d95c89..46edc1aef0 100644 --- a/pkg/server/http/option.go +++ b/pkg/server/http/option.go @@ -15,13 +15,14 @@ type Option func(o *Options) // Options defines the available options for this package. type Options struct { - Logger log.Logger - Context context.Context - Config *config.Config - Handler http.Handler - Metrics *metrics.Metrics - Flags []cli.Flag - Namespace string + Logger log.Logger + Context context.Context + Config *config.Config + Handler http.Handler + Metrics *metrics.Metrics + Flags []cli.Flag + Namespace string + Middlewares []func(handler http.Handler) http.Handler } // newOptions initializes the available default options. @@ -83,3 +84,10 @@ func Handler(h http.Handler) Option { o.Handler = h } } + +// Middlewares provides a function to register middlewares +func Middlewares(val ...func(handler http.Handler) http.Handler) Option { + return func(o *Options) { + o.Middlewares = val + } +} diff --git a/pkg/server/http/server.go b/pkg/server/http/server.go index b78082bc95..3fd022e502 100644 --- a/pkg/server/http/server.go +++ b/pkg/server/http/server.go @@ -5,6 +5,7 @@ import ( svc "github.com/owncloud/ocis-pkg/v2/service/http" "github.com/owncloud/ocis-proxy/pkg/crypto" "github.com/owncloud/ocis-proxy/pkg/version" + "net/http" "os" ) @@ -40,7 +41,6 @@ func Server(opts ...Option) (svc.Service, error) { service := svc.NewService( svc.Name("web.proxy"), - svc.Handler(options.Handler), svc.TLSConfig(tlsConfig), svc.Logger(options.Logger), svc.Namespace(options.Namespace), @@ -48,6 +48,11 @@ func Server(opts ...Option) (svc.Service, error) { svc.Address(options.Config.HTTP.Addr), svc.Context(options.Context), svc.Flags(options.Flags...), + svc.Handler(applyMiddlewares( + options.Handler, + options.Middlewares..., + ), + ), ) if err := service.Init(); err != nil { @@ -56,3 +61,12 @@ func Server(opts ...Option) (svc.Service, error) { return service, nil } + +func applyMiddlewares(h http.Handler, mws ...func(handler http.Handler) http.Handler) http.Handler { + var han = h + for _, mw := range mws { + han = mw(han) + } + + return han +}