diff --git a/changelog/unreleased/remove-well-known-oidc-middleware.md b/changelog/unreleased/remove-well-known-oidc-middleware.md new file mode 100644 index 0000000000..888dd40b40 --- /dev/null +++ b/changelog/unreleased/remove-well-known-oidc-middleware.md @@ -0,0 +1,10 @@ +Enhancement: Introduce staticroutes package & remove well-known OIDC middleware + +We have introduced a new static routes package to the proxy. This package +is responsible for serving static files and oidc well-known endpoint `/.well-known/openid-configuration`. +We have removed the well-known middleware for OIDC and moved it +to the newly introduced static routes module in the proxy. + +https://github.com/owncloud/ocis/issues/6095 +https://github.com/owncloud/ocis/pull/8541 + diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index a3739163e9..41ff934228 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -3,15 +3,13 @@ package command import ( "context" "crypto/tls" - "errors" "fmt" + "github.com/owncloud/ocis/v2/services/proxy/pkg/staticroutes" "net/http" "os" "time" - "github.com/go-chi/chi/v5" chimiddleware "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/render" "github.com/justinas/alice" "github.com/oklog/run" "github.com/urfave/cli/v2" @@ -135,13 +133,13 @@ func Server(cfg *config.Config) *cli.Command { proxy.Config(cfg), ) - lh := StaticRouteHandler{ - prefix: cfg.HTTP.Root, - userInfoCache: userInfoCache, - logger: logger, - config: *cfg, - oidcClient: oidcClient, - proxy: rp, + lh := staticroutes.StaticRouteHandler{ + Prefix: cfg.HTTP.Root, + UserInfoCache: userInfoCache, + Logger: logger, + Config: *cfg, + OidcClient: oidcClient, + Proxy: rp, } if err != nil { return fmt.Errorf("failed to initialize reverse proxy: %w", err) @@ -150,7 +148,7 @@ func Server(cfg *config.Config) *cli.Command { { middlewares := loadMiddlewares(ctx, logger, cfg, userInfoCache, signingKeyStore, traceProvider, *m) server, err := proxyHTTP.Server( - proxyHTTP.Handler(lh.handler()), + proxyHTTP.Handler(lh.Handler()), proxyHTTP.Logger(logger), proxyHTTP.Context(ctx), proxyHTTP.Config(cfg), @@ -201,94 +199,6 @@ func Server(cfg *config.Config) *cli.Command { } } -// StaticRouteHandler defines a Route Handler for static routes -type StaticRouteHandler struct { - prefix string - proxy http.Handler - userInfoCache microstore.Store - logger log.Logger - config config.Config - oidcClient oidc.OIDCClient -} - -func (h *StaticRouteHandler) handler() http.Handler { - m := chi.NewMux() - m.Route(h.prefix, func(r chi.Router) { - // Wrapper for backchannel logout - r.Post("/backchannel_logout", h.backchannelLogout) - - // TODO: migrate oidc well knowns here in a second wrapper - - // Send all requests to the proxy handler - r.HandleFunc("/*", h.proxy.ServeHTTP) - }) - - // Also send requests for methods unknown to chi to the proxy handler as well - m.MethodNotAllowed(h.proxy.ServeHTTP) - - return m -} - -type jse struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description"` -} - -// handle backchannel logout requests as per https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest -func (h *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) { - // parse the application/x-www-form-urlencoded POST request - logger := h.logger.SubloggerWithRequestID(r.Context()) - if err := r.ParseForm(); err != nil { - logger.Warn().Err(err).Msg("ParseForm failed") - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) - return - } - - logoutToken, err := h.oidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token")) - if err != nil { - logger.Warn().Err(err).Msg("VerifyLogoutToken failed") - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) - return - } - - records, err := h.userInfoCache.Read(logoutToken.SessionId) - if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 { - render.Status(r, http.StatusOK) - render.JSON(w, r, nil) - return - } - - if err != nil { - logger.Error().Err(err).Msg("Error reading userinfo cache") - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) - return - } - - for _, record := range records { - err = h.userInfoCache.Delete(string(record.Value)) - if err != nil && !errors.Is(err, microstore.ErrNotFound) { - // Spec requires us to return a 400 BadRequest when the session could not be destroyed - logger.Err(err).Msg("could not delete user info from cache") - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) - return - } - logger.Debug().Msg("Deleted userinfo from cache") - } - - // we can ignore errors when cleaning up the lookup table - err = h.userInfoCache.Delete(logoutToken.SessionId) - if err != nil { - logger.Debug().Err(err).Msg("Failed to cleanup sessionid lookup entry") - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, nil) -} - func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config, userInfoCache, signingKeyStore microstore.Store, traceProvider trace.TracerProvider, metrics metrics.Metrics) alice.Chain { rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", cfg.GrpcClient) policiesProviderClient := policiessvc.NewPoliciesProviderService("com.owncloud.api.policies", cfg.GrpcClient) @@ -403,11 +313,6 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config, chimiddleware.RequestID, middleware.AccessLog(logger), middleware.HTTPSRedirect, - middleware.OIDCWellKnownRewrite( - logger, cfg.OIDC.Issuer, - cfg.OIDC.RewriteWellKnown, - oidcHTTPClient, - ), router.Middleware(cfg.PolicySelector, cfg.Policies, logger), middleware.Authentication( authenticators, diff --git a/services/proxy/pkg/middleware/oidc_well-known.go b/services/proxy/pkg/middleware/oidc_well-known.go deleted file mode 100644 index 9957d83822..0000000000 --- a/services/proxy/pkg/middleware/oidc_well-known.go +++ /dev/null @@ -1,56 +0,0 @@ -package middleware - -import ( - "io" - "net/http" - "net/url" - "path" - - "github.com/owncloud/ocis/v2/ocis-pkg/log" -) - -var ( - wellKnownPath = "/.well-known/openid-configuration" -) - -// OIDCWellKnownRewrite is a middleware that rewrites the /.well-known/openid-configuration endpoint for external IDPs. -func OIDCWellKnownRewrite(logger log.Logger, oidcISS string, rewrite bool, oidcClient *http.Client) func(http.Handler) http.Handler { - - oidcURL, _ := url.Parse(oidcISS) - oidcURL.Path = path.Join(oidcURL.Path, wellKnownPath) - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if rewrite && path.Clean(r.URL.Path) == wellKnownPath { - - wellKnownRes, err := oidcClient.Get(oidcURL.String()) - if err != nil { - logger.Error(). - Err(err). - Str("middleware", "oidc wellknown rewrite"). - Str("url", oidcURL.String()). - Msg("get information from url failed") - w.WriteHeader(http.StatusInternalServerError) - return - } - - defer wellKnownRes.Body.Close() - - copyHeader(w.Header(), wellKnownRes.Header) - w.WriteHeader(wellKnownRes.StatusCode) - io.Copy(w, wellKnownRes.Body) - - return - } - next.ServeHTTP(w, r) - }) - } -} - -func copyHeader(dst, src http.Header) { - for k, vv := range src { - for _, v := range vv { - dst.Add(k, v) - } - } -} diff --git a/services/proxy/pkg/staticroutes/backchannellogout.go b/services/proxy/pkg/staticroutes/backchannellogout.go new file mode 100644 index 0000000000..941c5cae70 --- /dev/null +++ b/services/proxy/pkg/staticroutes/backchannellogout.go @@ -0,0 +1,64 @@ +package staticroutes + +import ( + "net/http" + + "github.com/go-chi/render" + "github.com/pkg/errors" + microstore "go-micro.dev/v4/store" +) + +// handle backchannel logout requests as per https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest +func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) { + // parse the application/x-www-form-urlencoded POST request + logger := s.Logger.SubloggerWithRequestID(r.Context()) + if err := r.ParseForm(); err != nil { + logger.Warn().Err(err).Msg("ParseForm failed") + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) + return + } + + logoutToken, err := s.OidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token")) + if err != nil { + logger.Warn().Err(err).Msg("VerifyLogoutToken failed") + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) + return + } + + records, err := s.UserInfoCache.Read(logoutToken.SessionId) + if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 { + render.Status(r, http.StatusOK) + render.JSON(w, r, nil) + return + } + + if err != nil { + logger.Error().Err(err).Msg("Error reading userinfo cache") + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) + return + } + + for _, record := range records { + err = s.UserInfoCache.Delete(string(record.Value)) + if err != nil && !errors.Is(err, microstore.ErrNotFound) { + // Spec requires us to return a 400 BadRequest when the session could not be destroyed + logger.Err(err).Msg("could not delete user info from cache") + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) + return + } + logger.Debug().Msg("Deleted userinfo from cache") + } + + // we can ignore errors when cleaning up the lookup table + err = s.UserInfoCache.Delete(logoutToken.SessionId) + if err != nil { + logger.Debug().Err(err).Msg("Failed to cleanup sessionid lookup entry") + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, nil) +} diff --git a/services/proxy/pkg/staticroutes/oidc_well-known.go b/services/proxy/pkg/staticroutes/oidc_well-known.go new file mode 100644 index 0000000000..2cbd4bff94 --- /dev/null +++ b/services/proxy/pkg/staticroutes/oidc_well-known.go @@ -0,0 +1,45 @@ +package staticroutes + +import ( + "io" + "net/http" +) + +var ( + wellKnownPath = "/.well-known/openid-configuration" +) + +// OIDCWellKnownRewrite is a handler that rewrites the /.well-known/openid-configuration endpoint for external IDPs. +func (s *StaticRouteHandler) oIDCWellKnownRewrite(w http.ResponseWriter, r *http.Request) { + wellKnownRes, err := s.OidcHttpClient.Get(s.oidcURL.String()) + if err != nil { + s.Logger.Error(). + Err(err). + Str("handler", "oidc wellknown rewrite"). + Str("url", s.oidcURL.String()). + Msg("get information from url failed") + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer wellKnownRes.Body.Close() + + copyHeader(w.Header(), wellKnownRes.Header) + w.WriteHeader(wellKnownRes.StatusCode) + _, err = io.Copy(w, wellKnownRes.Body) + if err != nil { + s.Logger.Error(). + Err(err). + Str("handler", "oidc wellknown rewrite"). + Msg("copying response body failed") + + } +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} diff --git a/services/proxy/pkg/staticroutes/staticroutes.go b/services/proxy/pkg/staticroutes/staticroutes.go new file mode 100644 index 0000000000..e131d3b2e6 --- /dev/null +++ b/services/proxy/pkg/staticroutes/staticroutes.go @@ -0,0 +1,55 @@ +package staticroutes + +import ( + "net/http" + "net/url" + "path" + + "github.com/go-chi/chi/v5" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/ocis-pkg/oidc" + "github.com/owncloud/ocis/v2/services/proxy/pkg/config" + microstore "go-micro.dev/v4/store" +) + +// StaticRouteHandler defines a Route Handler for static routes +type StaticRouteHandler struct { + Prefix string + Proxy http.Handler + UserInfoCache microstore.Store + Logger log.Logger + Config config.Config + OidcClient oidc.OIDCClient + OidcHttpClient *http.Client + + oidcURL *url.URL +} + +type jse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +func (s *StaticRouteHandler) Handler() http.Handler { + s.oidcURL, _ = url.Parse(s.Config.OIDC.Issuer) + s.oidcURL.Path = path.Join(s.oidcURL.Path, wellKnownPath) + m := chi.NewMux() + m.Route(s.Prefix, func(r chi.Router) { + + // Wrapper for backchannel logout + r.Post("/backchannel_logout", s.backchannelLogout) + + // openid .well-known + if s.Config.OIDC.RewriteWellKnown { + r.Get("/.well-known/openid-configuration", s.oIDCWellKnownRewrite) + } + + // Send all requests to the proxy handler + r.HandleFunc("/*", s.Proxy.ServeHTTP) + }) + + // Also send requests for methods unknown to chi to the proxy handler as well + m.MethodNotAllowed(s.Proxy.ServeHTTP) + + return m +}