mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-31 01:10:20 -06:00
The "aud" claim of the logout token is supposed to contain the client-id of the client for which the token was issued. Our current implementation of validating that claim is somewhat broken. We only allow to configure a single value for the allowed client id. But we have different client-ids accessing oCIS. This completely removes the current validation of the `aud` claim until we come up with a working solution. As we currently require a session id to be present in the logout token the risk not validating the `aud` claim is pretty low. Related: #6149
415 lines
13 KiB
Go
415 lines
13 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
|
"github.com/cs3org/reva/v2/pkg/store"
|
|
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
|
|
"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/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
|
pkgmiddleware "github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
|
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
|
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/config/parser"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/logging"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/metrics"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/middleware"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/proxy"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/router"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/server/debug"
|
|
proxyHTTP "github.com/owncloud/ocis/v2/services/proxy/pkg/server/http"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/tracing"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
|
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
|
|
"github.com/urfave/cli/v2"
|
|
microstore "go-micro.dev/v4/store"
|
|
)
|
|
|
|
// Server is the entrypoint for the server command.
|
|
func Server(cfg *config.Config) *cli.Command {
|
|
return &cli.Command{
|
|
Name: "server",
|
|
Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
|
|
Category: "server",
|
|
Before: func(c *cli.Context) error {
|
|
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
userInfoCache := store.Create(
|
|
store.Store(cfg.OIDC.UserinfoCache.Store),
|
|
store.TTL(cfg.OIDC.UserinfoCache.TTL),
|
|
store.Size(cfg.OIDC.UserinfoCache.Size),
|
|
microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...),
|
|
microstore.Database(cfg.OIDC.UserinfoCache.Database),
|
|
microstore.Table(cfg.OIDC.UserinfoCache.Table),
|
|
)
|
|
|
|
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
|
err := tracing.Configure(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = grpc.Configure(grpc.GetClientOptions(cfg.GRPCClientTLS)...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var oidcHTTPClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
InsecureSkipVerify: cfg.OIDC.Insecure, //nolint:gosec
|
|
},
|
|
DisableKeepAlives: true,
|
|
},
|
|
Timeout: time.Second * 10,
|
|
}
|
|
|
|
oidcClient := oidc.NewOIDCClient(
|
|
oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
|
|
oidc.WithLogger(logger),
|
|
oidc.WithHTTPClient(oidcHTTPClient),
|
|
oidc.WithOidcIssuer(cfg.OIDC.Issuer),
|
|
oidc.WithJWKSOptions(cfg.OIDC.JWKS),
|
|
)
|
|
|
|
var (
|
|
m = metrics.New()
|
|
)
|
|
|
|
gr := run.Group{}
|
|
ctx, cancel := func() (context.Context, context.CancelFunc) {
|
|
if cfg.Context == nil {
|
|
return context.WithCancel(context.Background())
|
|
}
|
|
return context.WithCancel(cfg.Context)
|
|
}()
|
|
|
|
defer cancel()
|
|
|
|
m.BuildInfo.WithLabelValues(version.GetString()).Set(1)
|
|
|
|
rp, err := proxy.NewMultiHostReverseProxy(
|
|
proxy.Logger(logger),
|
|
proxy.Config(cfg),
|
|
)
|
|
|
|
lh := 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)
|
|
}
|
|
|
|
{
|
|
middlewares := loadMiddlewares(ctx, logger, cfg, userInfoCache)
|
|
server, err := proxyHTTP.Server(
|
|
proxyHTTP.Handler(lh.handler()),
|
|
proxyHTTP.Logger(logger),
|
|
proxyHTTP.Context(ctx),
|
|
proxyHTTP.Config(cfg),
|
|
proxyHTTP.Metrics(metrics.New()),
|
|
proxyHTTP.Middlewares(middlewares),
|
|
)
|
|
|
|
if err != nil {
|
|
logger.Error().
|
|
Err(err).
|
|
Str("server", "http").
|
|
Msg("Failed to initialize server")
|
|
|
|
return err
|
|
}
|
|
|
|
gr.Add(func() error {
|
|
return server.Run()
|
|
}, func(err error) {
|
|
logger.Error().
|
|
Err(err).
|
|
Str("server", "http").
|
|
Msg("Shutting down server")
|
|
|
|
cancel()
|
|
})
|
|
}
|
|
|
|
{
|
|
server, err := debug.Server(
|
|
debug.Logger(logger),
|
|
debug.Context(ctx),
|
|
debug.Config(cfg),
|
|
)
|
|
|
|
if err != nil {
|
|
logger.Error().Err(err).Str("server", "debug").Msg("Failed to initialize server")
|
|
return err
|
|
}
|
|
|
|
gr.Add(server.ListenAndServe, func(_ error) {
|
|
_ = server.Shutdown(ctx)
|
|
cancel()
|
|
})
|
|
}
|
|
|
|
return gr.Run()
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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
|
|
r.HandleFunc("/*", h.proxy.ServeHTTP)
|
|
|
|
})
|
|
// This is commented out due to a race issue in chi
|
|
//var methods = []string{"PROPFIND", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "REPORT"}
|
|
//for _, k := range methods {
|
|
// chi.RegisterMethod(k)
|
|
//}
|
|
|
|
// To avoid using the chi.RegisterMethod() this is basically a catchAll for all HTTP Methods that are not
|
|
// covered in chi by default
|
|
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 microstore.Store) alice.Chain {
|
|
rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
|
|
revaClient, err := pool.GetGatewayServiceClient(cfg.Reva.Address, cfg.Reva.GetRevaOptions()...)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msg("Failed to get gateway client")
|
|
}
|
|
tokenManager, err := jwt.New(map[string]interface{}{
|
|
"secret": cfg.TokenManager.JWTSecret,
|
|
})
|
|
if err != nil {
|
|
logger.Fatal().Err(err).
|
|
Msg("Failed to create token manager")
|
|
}
|
|
autoProvsionCreator := autoprovision.NewCreator(autoprovision.WithTokenManager(tokenManager))
|
|
var userProvider backend.UserBackend
|
|
switch cfg.AccountBackend {
|
|
case "cs3":
|
|
|
|
userProvider = backend.NewCS3UserBackend(
|
|
backend.WithLogger(logger),
|
|
backend.WithRevaAuthenticator(revaClient),
|
|
backend.WithMachineAuthAPIKey(cfg.MachineAuthAPIKey),
|
|
backend.WithOIDCissuer(cfg.OIDC.Issuer),
|
|
backend.WithAutoProvisonCreator(autoProvsionCreator),
|
|
)
|
|
default:
|
|
logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend)
|
|
}
|
|
|
|
var roleAssigner userroles.UserRoleAssigner
|
|
switch cfg.RoleAssignment.Driver {
|
|
case "default":
|
|
roleAssigner = userroles.NewDefaultRoleAssigner(
|
|
userroles.WithRoleService(rolesClient),
|
|
userroles.WithLogger(logger),
|
|
)
|
|
case "oidc":
|
|
roleAssigner = userroles.NewOIDCRoleAssigner(
|
|
userroles.WithRoleService(rolesClient),
|
|
userroles.WithLogger(logger),
|
|
userroles.WithRolesClaim(cfg.RoleAssignment.OIDCRoleMapper.RoleClaim),
|
|
userroles.WithRoleMapping(cfg.RoleAssignment.OIDCRoleMapper.RolesMap),
|
|
userroles.WithAutoProvisonCreator(autoProvsionCreator),
|
|
)
|
|
default:
|
|
logger.Fatal().Msgf("Invalid role assignment driver '%s'", cfg.RoleAssignment.Driver)
|
|
}
|
|
|
|
storeClient := storesvc.NewStoreService("com.owncloud.api.store", grpc.DefaultClient())
|
|
if err != nil {
|
|
logger.Error().Err(err).
|
|
Str("gateway", cfg.Reva.Address).
|
|
Msg("Failed to create reva gateway service client")
|
|
}
|
|
|
|
var oidcHTTPClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
InsecureSkipVerify: cfg.OIDC.Insecure, //nolint:gosec
|
|
},
|
|
DisableKeepAlives: true,
|
|
},
|
|
Timeout: time.Second * 10,
|
|
}
|
|
|
|
var authenticators []middleware.Authenticator
|
|
if cfg.EnableBasicAuth {
|
|
logger.Warn().Msg("basic auth enabled, use only for testing or development")
|
|
authenticators = append(authenticators, middleware.BasicAuthenticator{
|
|
Logger: logger,
|
|
UserProvider: userProvider,
|
|
})
|
|
}
|
|
|
|
authenticators = append(authenticators, middleware.NewOIDCAuthenticator(
|
|
middleware.Logger(logger),
|
|
middleware.UserInfoCache(userInfoCache),
|
|
middleware.DefaultAccessTokenTTL(cfg.OIDC.UserinfoCache.TTL),
|
|
middleware.HTTPClient(oidcHTTPClient),
|
|
middleware.OIDCIss(cfg.OIDC.Issuer),
|
|
middleware.OIDCClient(oidc.NewOIDCClient(
|
|
oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
|
|
oidc.WithLogger(logger),
|
|
oidc.WithHTTPClient(oidcHTTPClient),
|
|
oidc.WithOidcIssuer(cfg.OIDC.Issuer),
|
|
oidc.WithJWKSOptions(cfg.OIDC.JWKS),
|
|
)),
|
|
))
|
|
authenticators = append(authenticators, middleware.PublicShareAuthenticator{
|
|
Logger: logger,
|
|
RevaGatewayClient: revaClient,
|
|
})
|
|
authenticators = append(authenticators, middleware.SignedURLAuthenticator{
|
|
Logger: logger,
|
|
PreSignedURLConfig: cfg.PreSignedURL,
|
|
UserProvider: userProvider,
|
|
UserRoleAssigner: roleAssigner,
|
|
Store: storeClient,
|
|
})
|
|
|
|
return alice.New(
|
|
// first make sure we log all requests and redirect to https if necessary
|
|
pkgmiddleware.TraceContext,
|
|
chimiddleware.RealIP,
|
|
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,
|
|
middleware.CredentialsByUserAgent(cfg.AuthMiddleware.CredentialsByUserAgent),
|
|
middleware.Logger(logger),
|
|
middleware.OIDCIss(cfg.OIDC.Issuer),
|
|
middleware.EnableBasicAuth(cfg.EnableBasicAuth),
|
|
),
|
|
middleware.AccountResolver(
|
|
middleware.Logger(logger),
|
|
middleware.UserProvider(userProvider),
|
|
middleware.UserRoleAssigner(roleAssigner),
|
|
middleware.UserOIDCClaim(cfg.UserOIDCClaim),
|
|
middleware.UserCS3Claim(cfg.UserCS3Claim),
|
|
middleware.AutoprovisionAccounts(cfg.AutoprovisionAccounts),
|
|
),
|
|
middleware.SelectorCookie(
|
|
middleware.Logger(logger),
|
|
middleware.PolicySelectorConfig(*cfg.PolicySelector),
|
|
),
|
|
middleware.Policies(logger, cfg.PoliciesMiddleware.Query),
|
|
// finally, trigger home creation when a user logs in
|
|
middleware.CreateHome(
|
|
middleware.Logger(logger),
|
|
middleware.RevaGatewayClient(revaClient),
|
|
middleware.RoleQuotas(cfg.RoleQuotas),
|
|
),
|
|
)
|
|
}
|