Files
opencloud/services/proxy/pkg/command/server.go
Ralf Haferkamp b7990875c1 oidc: Remove "aud" claim validation of logout tokens (#6156)
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
2023-04-27 10:34:09 +02:00

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),
),
)
}