From 66d220ff442f44d579919cfde2c54f69e2353495 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Fri, 13 Feb 2026 20:41:16 +0100 Subject: [PATCH] enhancement: finalize backchannel logout --- services/proxy/pkg/command/server.go | 2 + .../pkg/config/defaults/defaultconfig.go | 2 + services/proxy/pkg/middleware/oidc_auth.go | 48 ++-- .../pkg/staticroutes/backchannellogout.go | 131 ++++++----- .../backchannellogout/backchannellogout.go | 125 ++++++++++ .../backchannellogout_test.go | 213 ++++++++++++++++++ .../proxy/pkg/staticroutes/staticroutes.go | 3 +- 7 files changed, 430 insertions(+), 94 deletions(-) create mode 100644 services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go create mode 100644 services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout_test.go diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index a1f2f842dd..b3e8332480 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -11,6 +11,7 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/justinas/alice" + "github.com/opencloud-eu/opencloud/pkg/config/configlog" "github.com/opencloud-eu/opencloud/pkg/generators" "github.com/opencloud-eu/opencloud/pkg/log" @@ -73,6 +74,7 @@ func Server(cfg *config.Config) *cli.Command { microstore.Nodes(cfg.PreSignedURL.SigningKeys.Nodes...), microstore.Database("proxy"), microstore.Table("signing-keys"), + store.DisablePersistence(cfg.PreSignedURL.SigningKeys.DisablePersistence), store.Authentication(cfg.PreSignedURL.SigningKeys.AuthUsername, cfg.PreSignedURL.SigningKeys.AuthPassword), ) diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 9e0985003a..bbdd82a272 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -45,6 +45,8 @@ func DefaultConfig() *config.Config { AccessTokenVerifyMethod: config.AccessTokenVerificationJWT, SkipUserInfo: false, UserinfoCache: &config.Cache{ + // toDo: + // check if "memory" is the right default or if "nats-js-kv" is a better match Store: "memory", Nodes: []string{"127.0.0.1:9233"}, Database: "cache-userinfo", diff --git a/services/proxy/pkg/middleware/oidc_auth.go b/services/proxy/pkg/middleware/oidc_auth.go index dcb82f54d4..342e5bbbe9 100644 --- a/services/proxy/pkg/middleware/oidc_auth.go +++ b/services/proxy/pkg/middleware/oidc_auth.go @@ -3,20 +3,20 @@ package middleware import ( "context" "encoding/base64" - "fmt" "net" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v5" - "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/pkg/oidc" "github.com/pkg/errors" "github.com/vmihailenco/msgpack/v5" - store "go-micro.dev/v4/store" + "go-micro.dev/v4/store" "golang.org/x/crypto/sha3" "golang.org/x/oauth2" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/oidc" ) const ( @@ -115,28 +115,26 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri m.Logger.Error().Err(err).Msg("failed to write to userinfo cache") } - if sid := aClaims.SessionID; sid != "" { - // reuse user cache for session id lookup - err = m.userInfoCache.Write(&store.Record{ - Key: sid, - Value: []byte(encodedHash), - Expiry: time.Until(expiration), - }) - if err != nil { - m.Logger.Error().Err(err).Msg("failed to write session lookup cache") - } + subject, sessionId := aClaims.Subject, aClaims.SessionID + // if no session id is present, we can't do a session lookup, + // so we can skip the cache entry for that. + if sessionId == "" { + return + } - // create an additional entry mapping subject to session id - if sub := aClaims.Subject; sub != "" { - err = m.userInfoCache.Write(&store.Record{ - Key: fmt.Sprintf("%s.%s", sub, sid), - Value: []byte(sid), - Expiry: time.Until(expiration), - }) - if err != nil { - m.Logger.Error().Err(err).Msg("failed to write subject lookup cache") - } - } + // if the claim has no subject, we can leave it empty, + // it's important to keep the dot in the key to prevent + // sufix and prefix exploration in the cache. + // + // ok: {key: ".sessionId"} + // ok: {key: "subject.sessionId"} + key := strings.Join([]string{subject, sessionId}, ".") + if err := m.userInfoCache.Write(&store.Record{ + Key: key, + Value: []byte(encodedHash), + Expiry: time.Until(expiration), + }); err != nil { + m.Logger.Error().Err(err).Msg("failed to write session lookup cache") } } }() diff --git a/services/proxy/pkg/staticroutes/backchannellogout.go b/services/proxy/pkg/staticroutes/backchannellogout.go index eb5574112c..4dee667607 100644 --- a/services/proxy/pkg/staticroutes/backchannellogout.go +++ b/services/proxy/pkg/staticroutes/backchannellogout.go @@ -7,15 +7,24 @@ import ( "strings" "github.com/go-chi/render" - "github.com/opencloud-eu/opencloud/pkg/oidc" - "github.com/opencloud-eu/reva/v2/pkg/events" - "github.com/opencloud-eu/reva/v2/pkg/utils" "github.com/pkg/errors" "github.com/vmihailenco/msgpack/v5" microstore "go-micro.dev/v4/store" + + bcl "github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes/internal/backchannellogout" + "github.com/opencloud-eu/reva/v2/pkg/events" + "github.com/opencloud-eu/reva/v2/pkg/utils" ) -// handle backchannel logout requests as per https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest +// BackchannelLogout handles backchannel logout requests from the identity provider and invalidates the related sessions in the cache +// spec: https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest +// +// toDo: +// - keyCloak "Sign out all active sessions" fails to log out, no incoming request +// - if the keycloak setting "Backchannel logout session required" is disabled, +// we resolve the session by the subject which can lead to multiple session records, +// we then send a logout event to each connected client and delete our stored record (subject.session & claim). +// but the session still exists in the identity provider. 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()) @@ -26,13 +35,6 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re return } - if r.PostFormValue("logout_token") == "" { - logger.Warn().Msg("logout_token is missing") - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: "logout_token is missing"}) - return - } - logoutToken, err := s.OidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token")) if err != nil { logger.Warn().Err(err).Msg("VerifyLogoutToken failed") @@ -41,70 +43,63 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re return } - var cacheKeys map[string]bool - - if strings.TrimSpace(logoutToken.SessionId) != "" { - cacheKeys[logoutToken.SessionId] = true - } else if strings.TrimSpace(logoutToken.Subject) != "" { - records, err := s.UserInfoCache.Read(fmt.Sprintf("%s.*", logoutToken.Subject)) - if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) - 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 { - cacheKeys[string(record.Value)] = true - cacheKeys[record.Key] = false - } - } else { - logger.Warn().Msg("invalid logout token") + subject, session := strings.Join(strings.Fields(logoutToken.Subject), ""), strings.Join(strings.Fields(logoutToken.SessionId), "") + if subject == "" && session == "" { + jseErr := jse{Error: "invalid_request", ErrorDescription: "invalid logout token: subject and session id are empty"} + logger.Warn().Msg(jseErr.ErrorDescription) render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: "invalid logout token"}) + render.JSON(w, r, jseErr) return } - for key, isSID := range cacheKeys { - if isSID { - records, err := s.UserInfoCache.Read(key) - if err != nil && !errors.Is(err, microstore.ErrNotFound) { - 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) { - logger.Error().Err(err).Msg("Error deleting userinfo cache") - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) - return - } - } + requestSubjectAndSession := bcl.SuSe{Session: session, Subject: subject} + // find out which mode of backchannel logout we are in + // by checking if the session or subject is present in the token + logoutMode := bcl.GetLogoutMode(requestSubjectAndSession) + lookupRecords, err := bcl.GetLogoutRecords(requestSubjectAndSession, logoutMode, s.UserInfoCache) + if errors.Is(err, microstore.ErrNotFound) || len(lookupRecords) == 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 lookupRecords { + // the record key is in the format "subject.session" or ".session" + // the record value is the key of the record that contains the claim in its value + key, value := record.Key, string(record.Value) + + subjectSession, ok := bcl.NewSuSe(key) + if !ok { + logger.Warn().Msgf("invalid logout record key: %s", key) + continue } - err = s.UserInfoCache.Delete(key) + err := s.publishBackchannelLogoutEvent(r.Context(), subjectSession.Session, value) + if err != nil { + s.Logger.Warn().Err(err).Msg("could not publish backchannel logout event") + } + + err = s.UserInfoCache.Delete(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(fmt.Errorf("could not delete session from cache (%s)", key).Error()) - // We only return on requests that do only attempt to destroy a single session, not multiple + // we have to return a 400 BadRequest when we fail to delete the session + // https://openid.net/specs/openid-connect-backchannel-1_0.html#rfc.section.2.8 + 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 } - if isSID { - err := s.publishBackchannelLogoutEvent(r.Context(), key, logoutToken) - if err != nil { - s.Logger.Warn().Err(err).Msg("could not publish backchannel logout event") - } + + // we can ignore errors when deleting the lookup record + err = s.UserInfoCache.Delete(key) + if err != nil { + logger.Debug().Err(err).Msg("Failed to cleanup sessionid lookup entry") } - logger.Debug().Msg("Deleted userinfo from cache") } render.Status(r, http.StatusOK) @@ -112,20 +107,20 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re } // publishBackchannelLogoutEvent publishes a backchannel logout event when the callback revived from the identity provider -func (s StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, cacheKey string, logoutToken *oidc.LogoutToken) error { +func (s *StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, sessionId, claimKey string) error { if s.EventsPublisher == nil { return fmt.Errorf("the events publisher is not set") } - urecords, err := s.UserInfoCache.Read(cacheKey) + claimRecords, err := s.UserInfoCache.Read(claimKey) if err != nil { return fmt.Errorf("reading userinfo cache: %w", err) } - if len(urecords) == 0 { + if len(claimRecords) == 0 { return fmt.Errorf("userinfo not found") } var claims map[string]interface{} - if err = msgpack.Unmarshal(urecords[0].Value, &claims); err != nil { + if err = msgpack.Unmarshal(claimRecords[0].Value, &claims); err != nil { return fmt.Errorf("could not unmarshal userinfo: %w", err) } @@ -141,7 +136,7 @@ func (s StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, c e := events.BackchannelLogout{ Executant: user.GetId(), - SessionId: logoutToken.SessionId, + SessionId: sessionId, Timestamp: utils.TSNow(), } diff --git a/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go b/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go new file mode 100644 index 0000000000..92f9ab0d59 --- /dev/null +++ b/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go @@ -0,0 +1,125 @@ +// package backchannellogout provides functions to classify and lookup +// backchannel logout records from the cache store. + +package backchannellogout + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + microstore "go-micro.dev/v4/store" +) + +// SuSe 🦎 ;) is a struct that groups the subject and session together +// to prevent mix-ups for ('session, subject' || 'subject, session') +// return values. +type SuSe struct { + Subject string + Session string +} + +// NewSuSe parses the subject and session id from the given key and returns a SuSe struct +func NewSuSe(key string) (SuSe, bool) { + var subject, session string + switch keys := strings.Split(strings.Join(strings.Fields(key), ""), "."); { + case len(keys) == 2 && keys[0] == "" && keys[1] != "": + session = keys[1] + case len(keys) == 2 && keys[0] != "" && keys[1] == "": + subject = keys[0] + case len(keys) == 2 && keys[0] != "" && keys[1] != "": + subject = keys[0] + session = keys[1] + case len(keys) == 1 && keys[0] != "": + session = keys[0] + default: + return SuSe{}, false + } + + return SuSe{Session: session, Subject: subject}, true +} + +// LogoutMode defines the mode of backchannel logout, either by session or by subject +type LogoutMode int + +const ( + // LogoutModeUnknown is used when the logout mode cannot be determined + LogoutModeUnknown LogoutMode = iota + // LogoutModeSession is used when the logout mode is determined by the session id + LogoutModeSession + // LogoutModeSubject is used when the logout mode is determined by the subject + LogoutModeSubject +) + +// GetLogoutMode determines the backchannel logout mode based on the presence of subject and session in the SuSe struct +func GetLogoutMode(suse SuSe) LogoutMode { + switch { + case suse.Session != "": + return LogoutModeSession + case suse.Subject != "": + return LogoutModeSubject + default: + return LogoutModeUnknown + } +} + +// ErrSuspiciousCacheResult is returned when the cache result is suspicious +var ErrSuspiciousCacheResult = errors.New("suspicious cache result") + +// GetLogoutRecords retrieves the records from the user info cache based on the backchannel +// logout mode and the provided SuSe struct. +// it uses a seperator to prevent sufix and prefix exploration in the cache and checks +// if the retrieved records match the requested subject and or session id as well, to prevent false positives. +func GetLogoutRecords(suse SuSe, mode LogoutMode, store microstore.Store) ([]*microstore.Record, error) { + var key string + var opts []microstore.ReadOption + switch mode { + case LogoutModeSession: + // the dot at the beginning prevents sufix exploration in the cache, + // so only keys that end with '*.session' will be returned, but not '*sion'. + key = "." + suse.Session + opts = append(opts, microstore.ReadSuffix()) + case LogoutModeSubject: + // the dot at the end prevents prefix exploration in the cache, + // so only keys that start with 'subject.*' will be returned, but not 'sub*'. + key = suse.Subject + "." + opts = append(opts, microstore.ReadPrefix()) + default: + return nil, fmt.Errorf("%w: cannot determine logout mode", ErrSuspiciousCacheResult) + } + + records, err := store.Read(key, append(opts, microstore.ReadLimit(1000))...) + if err != nil { + return nil, err + } + + if len(records) == 0 { + return nil, microstore.ErrNotFound + } + + if mode == LogoutModeSession && len(records) > 1 { + return nil, fmt.Errorf("%w: multiple session records found", ErrSuspiciousCacheResult) + } + + // double-check if the found records match the requested subject and or session id as well, + // to prevent false positives. + for _, record := range records { + recordSuSe, ok := NewSuSe(record.Key) + if !ok { + return nil, microstore.ErrNotFound + } + + switch { + // in session mode, the session id must match, but the subject can be different + case mode == LogoutModeSession && suse.Session == recordSuSe.Session: + continue + // in subject mode, the subject must match, but the session id can be different + case mode == LogoutModeSubject && suse.Subject == recordSuSe.Subject: + continue + } + + return nil, fmt.Errorf("%w: record key does not match the requested subject or session", ErrSuspiciousCacheResult) + } + + return records, nil +} diff --git a/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout_test.go b/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout_test.go new file mode 100644 index 0000000000..9250a4885b --- /dev/null +++ b/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout_test.go @@ -0,0 +1,213 @@ +package backchannellogout + +import ( + "slices" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go-micro.dev/v4/store" +) + +func TestNewSuSe(t *testing.T) { + tests := []struct { + name string + key string + wantsuSe SuSe + wantOk bool + }{ + { + name: ".session", + key: ".session", + wantsuSe: SuSe{Session: "session", Subject: ""}, + wantOk: true, + }, + { + name: ".session", + key: ".session", + wantsuSe: SuSe{Session: "session", Subject: ""}, + wantOk: true, + }, + { + name: "session", + key: "session", + wantsuSe: SuSe{Session: "session", Subject: ""}, + wantOk: true, + }, + { + name: "subject.", + key: "subject.", + wantsuSe: SuSe{Session: "", Subject: "subject"}, + wantOk: true, + }, + { + name: "subject.session", + key: "subject.session", + wantsuSe: SuSe{Session: "session", Subject: "subject"}, + wantOk: true, + }, + { + name: "dot", + key: ".", + wantsuSe: SuSe{Session: "", Subject: ""}, + wantOk: false, + }, + { + name: "empty", + key: "", + wantsuSe: SuSe{Session: "", Subject: ""}, + wantOk: false, + }, + { + name: "whitespace . whitespace", + key: " . ", + wantsuSe: SuSe{Session: "", Subject: ""}, + wantOk: false, + }, + { + name: "whitespace subject whitespace . whitespace", + key: " subject . ", + wantsuSe: SuSe{Session: "", Subject: "subject"}, + wantOk: true, + }, + { + name: "whitespace . whitespace session whitespace", + key: " . session ", + wantsuSe: SuSe{Session: "session", Subject: ""}, + wantOk: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + suSe, ok := NewSuSe(tt.key) + require.Equal(t, tt.wantOk, ok) + require.Equal(t, tt.wantsuSe, suSe) + }) + } +} + +func TestGetLogoutMode(t *testing.T) { + tests := []struct { + name string + suSe SuSe + want LogoutMode + }{ + { + name: ".session", + suSe: SuSe{Session: "session", Subject: ""}, + want: LogoutModeSession, + }, + { + name: "subject.session", + suSe: SuSe{Session: "session", Subject: "subject"}, + want: LogoutModeSession, + }, + { + name: "subject.", + suSe: SuSe{Session: "", Subject: "subject"}, + want: LogoutModeSubject, + }, + { + name: "", + suSe: SuSe{Session: "", Subject: ""}, + want: LogoutModeUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mode := GetLogoutMode(tt.suSe) + require.Equal(t, tt.want, mode) + }) + } +} + +func TestGetLogoutRecords(t *testing.T) { + sessionStore := store.NewMemoryStore() + + recordClaimA := &store.Record{Key: "claim-a", Value: []byte("claim-a-data")} + recordClaimB := &store.Record{Key: "claim-b", Value: []byte("claim-b-data")} + recordClaimC := &store.Record{Key: "claim-c", Value: []byte("claim-c-data")} + recordClaimD := &store.Record{Key: "claim-d", Value: []byte("claim-d-data")} + recordSessionA := &store.Record{Key: "session-a", Value: []byte(recordClaimA.Key)} + recordSessionB := &store.Record{Key: "session-b", Value: []byte(recordClaimB.Key)} + recordSubjectASessionC := &store.Record{Key: "subject-a.session-c", Value: []byte(recordSessionA.Key)} + recordSubjectASessionD := &store.Record{Key: "subject-a.session-d", Value: []byte(recordSessionB.Key)} + + for _, r := range []*store.Record{ + recordClaimA, + recordClaimB, + recordClaimC, + recordClaimD, + recordSessionA, + recordSessionB, + recordSubjectASessionC, + recordSubjectASessionD, + } { + require.NoError(t, sessionStore.Write(r)) + } + + tests := []struct { + name string + suSe SuSe + mode LogoutMode + store store.Store + wantRecords []*store.Record + wantError error + }{ + { + name: "session-c", + suSe: SuSe{Session: "session-c"}, + mode: LogoutModeSession, + store: sessionStore, + wantRecords: []*store.Record{recordSubjectASessionC}, + }, + { + name: "ession-c", + suSe: SuSe{Session: "ession-c"}, + mode: LogoutModeSession, + store: sessionStore, + wantError: store.ErrNotFound, + wantRecords: []*store.Record{}, + }, + { + name: "subject-a", + suSe: SuSe{Subject: "subject-a"}, + mode: LogoutModeSubject, + store: sessionStore, + wantRecords: []*store.Record{recordSubjectASessionC, recordSubjectASessionD}, + }, + { + name: "subject-", + suSe: SuSe{Subject: "subject-"}, + mode: LogoutModeSubject, + store: sessionStore, + wantError: store.ErrNotFound, + wantRecords: []*store.Record{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + records, err := GetLogoutRecords(tt.suSe, tt.mode, tt.store) + require.ErrorIs(t, err, tt.wantError) + require.Len(t, records, len(tt.wantRecords)) + + sortRecords := func(r []*store.Record) []*store.Record { + slices.SortFunc(r, func(a, b *store.Record) int { + return strings.Compare(a.Key, b.Key) + }) + + return r + } + + records = sortRecords(records) + for i, wantRecords := range sortRecords(tt.wantRecords) { + require.True(t, len(records) >= i+1) + require.Equal(t, wantRecords.Key, records[i].Key) + require.Equal(t, wantRecords.Value, records[i].Value) + } + }) + } +} diff --git a/services/proxy/pkg/staticroutes/staticroutes.go b/services/proxy/pkg/staticroutes/staticroutes.go index f2f76740f1..61d4f0ca5b 100644 --- a/services/proxy/pkg/staticroutes/staticroutes.go +++ b/services/proxy/pkg/staticroutes/staticroutes.go @@ -4,12 +4,13 @@ import ( "net/http" "github.com/go-chi/chi/v5" + microstore "go-micro.dev/v4/store" + "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/oidc" "github.com/opencloud-eu/opencloud/services/proxy/pkg/config" "github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend" "github.com/opencloud-eu/reva/v2/pkg/events" - microstore "go-micro.dev/v4/store" ) // StaticRouteHandler defines a Route Handler for static routes