From fd614eacf1867212a8eefbc341f3b1ea1c671a7c Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Fri, 20 Feb 2026 16:45:38 +0100 Subject: [PATCH] fix: use base64 record keys to prevent separator clashes with subjects or sessionIds that contain a dot --- services/proxy/pkg/middleware/oidc_auth.go | 18 +- .../pkg/staticroutes/backchannellogout.go | 78 ++++--- .../backchannellogout/backchannellogout.go | 117 ++++++++--- .../backchannellogout_test.go | 198 +++++++++++------- 4 files changed, 265 insertions(+), 146 deletions(-) diff --git a/services/proxy/pkg/middleware/oidc_auth.go b/services/proxy/pkg/middleware/oidc_auth.go index 075f088b11..3a3ed29fdc 100644 --- a/services/proxy/pkg/middleware/oidc_auth.go +++ b/services/proxy/pkg/middleware/oidc_auth.go @@ -17,6 +17,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/oidc" + "github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes" ) const ( @@ -116,22 +117,21 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri m.Logger.Error().Err(err).Msg("failed to write to userinfo cache") } - subject, sessionId := strings.Join(strings.Fields(aClaims.Subject), ""), strings.Join(strings.Fields(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 - } - // 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."} // ok: {key: "subject.sessionId"} - key := strings.Join([]string{subject, sessionId}, ".") + subjectSessionKey, err := staticroutes.NewRecordKey(aClaims.Subject, aClaims.SessionID) + if err != nil { + m.Logger.Error().Err(err).Msg("failed to build subject.session") + return + } + if err := m.userInfoCache.Write(&store.Record{ - Key: key, + Key: subjectSessionKey, Value: []byte(encodedHash), Expiry: time.Until(expiration), }); err != nil { diff --git a/services/proxy/pkg/staticroutes/backchannellogout.go b/services/proxy/pkg/staticroutes/backchannellogout.go index 0d44e7cd1c..d7e10d5da8 100644 --- a/services/proxy/pkg/staticroutes/backchannellogout.go +++ b/services/proxy/pkg/staticroutes/backchannellogout.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "strings" "github.com/go-chi/render" "github.com/pkg/errors" @@ -16,6 +15,9 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/utils" ) +// NewRecordKey converts the subject and session to a base64 encoded key +var NewRecordKey = bcl.NewKey + // 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 // @@ -37,9 +39,6 @@ import ( // all sessions besides the one that triggered the backchannel logout continue to exist in the identity provider, // so the user will not be fully logged out until all sessions are logged out or expired. // this leads to the situation that web renders the logout view even if the instance is not fully logged out yet. -// -// toDo: -// - check logs and errors to not contain any sensitive information like session ids or user ids (keys too) func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) { logger := s.Logger.SubloggerWithRequestID(r.Context()) if err := r.ParseForm(); err != nil { @@ -51,22 +50,31 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re logoutToken, err := s.OidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token")) if err != nil { - logger.Warn().Err(err).Msg("VerifyLogoutToken failed") + msg := "failed to verify logout token" + logger.Warn().Err(err).Msg(msg) render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg}) return } - 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) + lookupKey, err := bcl.NewKey(logoutToken.Subject, logoutToken.SessionId) + if err != nil { + msg := "failed to build key from logout token" + logger.Warn().Err(err).Msg(msg) render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jseErr) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg}) + return + } + + requestSubjectAndSession, err := bcl.NewSuSe(lookupKey) + if err != nil { + msg := "failed to build subjec.session from lookupKey" + logger.Error().Err(err).Msg(msg) + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg}) 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) @@ -77,9 +85,10 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re return } if err != nil { - logger.Error().Err(err).Msg("Error reading userinfo cache") + msg := "failed to read userinfo cache" + logger.Error().Err(err).Msg(msg) render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg}) return } @@ -91,28 +100,36 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re subjectSession, err := bcl.NewSuSe(key) if err != nil { // never leak any key-related information - logger.Warn().Err(err).Msgf("invalid logout record key: %s", "XXX") + logger.Warn().Err(err).Msgf("failed to parse key: %s", key) continue } - if err := s.publishBackchannelLogoutEvent(r.Context(), subjectSession.Session, value); err != nil { - s.Logger.Warn().Err(err).Msg("could not publish backchannel logout event") + session, err := subjectSession.Session() + if err != nil { + logger.Warn().Err(err).Msgf("failed to read session for: %s", key) + continue + } + + if err := s.publishBackchannelLogoutEvent(r.Context(), session, value); err != nil { + s.Logger.Warn().Err(err).Msgf("failed to publish backchannel logout event for: %s", key) + continue } err = s.UserInfoCache.Delete(value) if err != nil && !errors.Is(err, microstore.ErrNotFound) { // 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") + msg := "failed to delete record" + s.Logger.Warn().Err(err).Msgf("%s for: %s", msg, key) render.Status(r, http.StatusBadRequest) - render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()}) + render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg}) return } // 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().Err(err).Msgf("failed to delete record for: %s", key) } } @@ -123,29 +140,30 @@ 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, sessionId, claimKey string) error { if s.EventsPublisher == nil { - return fmt.Errorf("the events publisher is not set") + return errors.New("events publisher not set") } + claimRecords, err := s.UserInfoCache.Read(claimKey) - if err != nil { - return fmt.Errorf("reading userinfo cache: %w", err) - } - if len(claimRecords) == 0 { - return fmt.Errorf("userinfo not found") + switch { + case err != nil: + return fmt.Errorf("failed to read userinfo cache: %w", err) + case len(claimRecords) == 0: + return fmt.Errorf("no claim found for key: %s", claimKey) } var claims map[string]interface{} if err = msgpack.Unmarshal(claimRecords[0].Value, &claims); err != nil { - return fmt.Errorf("could not unmarshal userinfo: %w", err) + return fmt.Errorf("failed to unmarshal claims: %w", err) } oidcClaim, ok := claims[s.Config.UserOIDCClaim].(string) if !ok { - return fmt.Errorf("could not get claim %w", err) + return fmt.Errorf("failed to get claim %w", err) } user, _, err := s.UserProvider.GetUserByClaims(ctx, s.Config.UserCS3Claim, oidcClaim) if err != nil || user.GetId() == nil { - return fmt.Errorf("could not get user by claims: %w", err) + return fmt.Errorf("failed to get user by claims: %w", err) } e := events.BackchannelLogout{ @@ -155,7 +173,7 @@ func (s *StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, } if err := events.Publish(ctx, s.EventsPublisher, e); err != nil { - return fmt.Errorf("could not publish user created event %w", err) + return fmt.Errorf("failed to publish user logout event %w", err) } return nil } diff --git a/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go b/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go index fb5c15ff36..1863047031 100644 --- a/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go +++ b/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout.go @@ -4,46 +4,97 @@ package backchannellogout import ( - "fmt" + "encoding/base64" + "errors" "strings" - "github.com/pkg/errors" microstore "go-micro.dev/v4/store" ) +// keyEncoding is the base64 encoding used for session and subject keys +var keyEncoding = base64.URLEncoding + +// ErrInvalidKey indicates that the provided key does not conform to the expected format. +var ErrInvalidKey = errors.New("invalid key format") + +// NewKey converts the subject and session to a base64 encoded key +func NewKey(subject, session string) (string, error) { + subjectSession := strings.Join([]string{ + keyEncoding.EncodeToString([]byte(subject)), + keyEncoding.EncodeToString([]byte(session)), + }, ".") + + if subjectSession == "." { + return "", ErrInvalidKey + } + + return subjectSession, nil +} + +// ErrDecoding is returned when decoding fails +var ErrDecoding = errors.New("failed to decode") + // 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 + encodedSubject string + encodedSession string } -// ErrInvalidSessionOrSubject is returned when the provided key does not match the expected key format -var ErrInvalidSessionOrSubject = errors.New("invalid session or subject") +// Subject decodes and returns the subject or an error +func (suse SuSe) Subject() (string, error) { + subject, err := keyEncoding.DecodeString(suse.encodedSubject) + if err != nil { + return "", errors.Join(errors.New("failed to decode subject"), ErrDecoding, err) + } + + return string(subject), nil +} + +// Session decodes and returns the session or an error +func (suse SuSe) Session() (string, error) { + subject, err := keyEncoding.DecodeString(suse.encodedSession) + if err != nil { + return "", errors.Join(errors.New("failed to decode session"), ErrDecoding, err) + } + + return string(subject), nil +} + +// ErrInvalidSubjectOrSession is returned when the provided key does not match the expected key format +var ErrInvalidSubjectOrSession = errors.New("invalid subject or session") // NewSuSe parses the subject and session id from the given key and returns a SuSe struct func NewSuSe(key string) (SuSe, error) { - var subject, session string + suse := SuSe{} switch keys := strings.Split(strings.Join(strings.Fields(key), ""), "."); { // key: '.session' case len(keys) == 2 && keys[0] == "" && keys[1] != "": - session = keys[1] + suse.encodedSession = keys[1] // key: 'subject.' case len(keys) == 2 && keys[0] != "" && keys[1] == "": - subject = keys[0] + suse.encodedSubject = keys[0] // key: 'subject.session' case len(keys) == 2 && keys[0] != "" && keys[1] != "": - subject = keys[0] - session = keys[1] + suse.encodedSubject = keys[0] + suse.encodedSession = keys[1] // key: 'session' case len(keys) == 1 && keys[0] != "": - session = keys[0] + suse.encodedSession = keys[0] default: - return SuSe{}, ErrInvalidSessionOrSubject + return suse, ErrInvalidSubjectOrSession } - return SuSe{Session: session, Subject: subject}, nil + if _, err := suse.Subject(); err != nil { + return suse, errors.Join(ErrInvalidSubjectOrSession, err) + } + + if _, err := suse.Session(); err != nil { + return suse, errors.Join(ErrInvalidSubjectOrSession, err) + } + + return suse, nil } // LogoutMode defines the mode of backchannel logout, either by session or by subject @@ -52,19 +103,19 @@ type LogoutMode int const ( // LogoutModeUndefined is used when the logout mode cannot be determined LogoutModeUndefined 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 + // LogoutModeSession is used when the logout mode is determined by the session id + LogoutModeSession ) // 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 != "": + case suse.encodedSession == "" && suse.encodedSubject != "": return LogoutModeSubject + case suse.encodedSession != "": + return LogoutModeSession default: return LogoutModeUndefined } @@ -81,18 +132,18 @@ func GetLogoutRecords(suse SuSe, mode LogoutMode, store microstore.Store) ([]*mi 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 + "." + key = suse.encodedSubject + "." opts = append(opts, microstore.ReadPrefix()) + 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.encodedSession + opts = append(opts, microstore.ReadSuffix()) default: - return nil, fmt.Errorf("%w: cannot determine logout mode", ErrSuspiciousCacheResult) + return nil, errors.Join(errors.New("cannot determine logout mode"), ErrSuspiciousCacheResult) } // the go micro memory store requires a limit to work, why??? @@ -106,7 +157,7 @@ func GetLogoutRecords(suse SuSe, mode LogoutMode, store microstore.Store) ([]*mi } if mode == LogoutModeSession && len(records) > 1 { - return nil, fmt.Errorf("%w: multiple session records found", ErrSuspiciousCacheResult) + return nil, errors.Join(errors.New("multiple session records found"), ErrSuspiciousCacheResult) } // double-check if the found records match the requested subject and or session id as well, @@ -115,19 +166,19 @@ func GetLogoutRecords(suse SuSe, mode LogoutMode, store microstore.Store) ([]*mi recordSuSe, err := NewSuSe(record.Key) if err != nil { // never leak any key-related information - return nil, fmt.Errorf("%w %w: failed to parse logout record key: %s", err, ErrSuspiciousCacheResult, "XXX") + return nil, errors.Join(errors.New("failed to parse key"), ErrSuspiciousCacheResult, err) } 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: + case mode == LogoutModeSubject && suse.encodedSubject == recordSuSe.encodedSubject: + continue + // in session mode, the session id must match, but the subject can be different + case mode == LogoutModeSession && suse.encodedSession == recordSuSe.encodedSession: continue } - return nil, fmt.Errorf("%w: record key does not match the requested subject or session", ErrSuspiciousCacheResult) + return nil, errors.Join(errors.New("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 index d442f06184..a653be2247 100644 --- a/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout_test.go +++ b/services/proxy/pkg/staticroutes/internal/backchannellogout/backchannellogout_test.go @@ -12,73 +12,123 @@ import ( "github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes/internal/backchannellogout/mocks" ) -func TestNewSuSe(t *testing.T) { +func mustNewKey(t *testing.T, subject, session string) string { + key, err := NewKey(subject, session) + require.NoError(t, err) + return key +} + +func mustNewSuSe(t *testing.T, subject, session string) SuSe { + suse, err := NewSuSe(mustNewKey(t, subject, session)) + require.NoError(t, err) + return suse +} + +func TestNewKey(t *testing.T) { tests := []struct { - name string - key string - wantSuSe SuSe - wantErr error + name string + subject string + session string + wantKey string + wantErr error }{ { - name: "key variation: '.session'", - key: ".session", - wantSuSe: SuSe{Session: "session", Subject: ""}, + name: "key variation: 'subject.session'", + subject: "subject", + session: "session", + wantKey: "c3ViamVjdA==.c2Vzc2lvbg==", }, { - name: "key variation: '.session'", - key: ".session", - wantSuSe: SuSe{Session: "session", Subject: ""}, + name: "key variation: 'subject.'", + subject: "subject", + wantKey: "c3ViamVjdA==.", }, { - name: "key variation: 'session'", - key: "session", - wantSuSe: SuSe{Session: "session", Subject: ""}, + name: "key variation: '.session'", + session: "session", + wantKey: ".c2Vzc2lvbg==", }, { - name: "key variation: 'subject.'", - key: "subject.", - wantSuSe: SuSe{Session: "", Subject: "subject"}, - }, - { - name: "key variation: 'subject.session'", - key: "subject.session", - wantSuSe: SuSe{Session: "session", Subject: "subject"}, - }, - { - name: "key variation: 'dot'", - key: ".", - wantSuSe: SuSe{Session: "", Subject: ""}, - wantErr: ErrInvalidSessionOrSubject, - }, - { - name: "key variation: 'empty'", - key: "", - wantSuSe: SuSe{Session: "", Subject: ""}, - wantErr: ErrInvalidSessionOrSubject, - }, - { - name: "key variation: 'whitespace . whitespace'", - key: " . ", - wantSuSe: SuSe{Session: "", Subject: ""}, - wantErr: ErrInvalidSessionOrSubject, - }, - { - name: "key variation: 'whitespace subject whitespace . whitespace'", - key: " subject . ", - wantSuSe: SuSe{Session: "", Subject: "subject"}, - }, - { - name: "key variation: 'whitespace . whitespace session whitespace'", - key: " . session ", - wantSuSe: SuSe{Session: "session", Subject: ""}, + name: "key variation: '.'", + wantErr: ErrInvalidKey, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - suSe, ok := NewSuSe(tt.key) - require.ErrorIs(t, tt.wantErr, ok) - require.Equal(t, tt.wantSuSe, suSe) + key, err := NewKey(tt.subject, tt.session) + require.ErrorIs(t, err, tt.wantErr) + require.Equal(t, tt.wantKey, key) + }) + } +} + +func TestNewSuSe(t *testing.T) { + tests := []struct { + name string + key string + wantSubject string + wantSession string + wantErr error + }{ + { + name: "key variation: '.session'", + key: mustNewKey(t, "", "session"), + wantSession: "session", + }, + { + name: "key variation: 'session'", + key: mustNewKey(t, "", "session"), + wantSession: "session", + }, + { + name: "key variation: 'subject.'", + key: mustNewKey(t, "subject", ""), + wantSubject: "subject", + }, + { + name: "key variation: 'subject.session'", + key: mustNewKey(t, "subject", "session"), + wantSubject: "subject", + wantSession: "session", + }, + { + name: "key variation: 'dot'", + key: ".", + wantErr: ErrInvalidSubjectOrSession, + }, + { + name: "key variation: 'empty'", + key: "", + wantErr: ErrInvalidSubjectOrSession, + }, + { + name: "key variation: string('subject.session')", + key: "subject.session", + wantErr: ErrInvalidSubjectOrSession, + }, + { + name: "key variation: string('subject.')", + key: "subject.", + wantErr: ErrInvalidSubjectOrSession, + }, + { + name: "key variation: string('.session')", + key: ".session", + wantErr: ErrInvalidSubjectOrSession, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + suSe, err := NewSuSe(tt.key) + require.ErrorIs(t, err, tt.wantErr) + + subject, _ := suSe.Subject() + require.Equal(t, tt.wantSubject, subject) + + session, _ := suSe.Session() + require.Equal(t, tt.wantSession, session) }) } } @@ -91,22 +141,22 @@ func TestGetLogoutMode(t *testing.T) { }{ { name: "key variation: '.session'", - suSe: SuSe{Session: "session", Subject: ""}, + suSe: mustNewSuSe(t, "", "session"), want: LogoutModeSession, }, { name: "key variation: 'subject.session'", - suSe: SuSe{Session: "session", Subject: "subject"}, + suSe: mustNewSuSe(t, "subject", "session"), want: LogoutModeSession, }, { name: "key variation: 'subject.'", - suSe: SuSe{Session: "", Subject: "subject"}, + suSe: mustNewSuSe(t, "subject", ""), want: LogoutModeSubject, }, { name: "key variation: 'empty'", - suSe: SuSe{Session: "", Subject: ""}, + suSe: SuSe{}, want: LogoutModeUndefined, }, } @@ -126,10 +176,10 @@ func TestGetLogoutRecords(t *testing.T) { 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)} + recordSessionA := &store.Record{Key: mustNewKey(t, "", "session-a"), Value: []byte(recordClaimA.Key)} + recordSessionB := &store.Record{Key: mustNewKey(t, "", "session-b"), Value: []byte(recordClaimB.Key)} + recordSubjectASessionC := &store.Record{Key: mustNewKey(t, "subject-a", "session-c"), Value: []byte(recordSessionA.Key)} + recordSubjectASessionD := &store.Record{Key: mustNewKey(t, "subject-a", "session-d"), Value: []byte(recordSessionA.Key)} for _, r := range []*store.Record{ recordClaimA, @@ -154,7 +204,7 @@ func TestGetLogoutRecords(t *testing.T) { }{ { name: "fails if mode is unknown", - suSe: SuSe{Session: "session-a"}, + suSe: mustNewSuSe(t, "", "session-a"), mode: LogoutModeUndefined, store: func(t *testing.T) store.Store { return sessionStore @@ -164,7 +214,7 @@ func TestGetLogoutRecords(t *testing.T) { }, { name: "fails if mode is any random int", - suSe: SuSe{Session: "session-a"}, + suSe: mustNewSuSe(t, "", "session-a"), mode: 999, store: func(t *testing.T) store.Store { return sessionStore @@ -173,7 +223,7 @@ func TestGetLogoutRecords(t *testing.T) { wantErrs: []error{ErrSuspiciousCacheResult}}, { name: "fails if multiple session records are found", - suSe: SuSe{Session: "session-a"}, + suSe: mustNewSuSe(t, "", "session-a"), mode: LogoutModeSession, store: func(t *testing.T) store.Store { s := mocks.NewStore(t) @@ -187,7 +237,7 @@ func TestGetLogoutRecords(t *testing.T) { wantErrs: []error{ErrSuspiciousCacheResult}}, { name: "fails if the record key is not ok", - suSe: SuSe{Session: "session-a"}, + suSe: mustNewSuSe(t, "", "session-a"), mode: LogoutModeSession, store: func(t *testing.T) store.Store { s := mocks.NewStore(t) @@ -197,11 +247,11 @@ func TestGetLogoutRecords(t *testing.T) { return s }, wantRecords: []*store.Record{}, - wantErrs: []error{ErrInvalidSessionOrSubject, ErrSuspiciousCacheResult}, + wantErrs: []error{ErrInvalidSubjectOrSession, ErrSuspiciousCacheResult}, }, { name: "fails if the session does not match the retrieved record", - suSe: SuSe{Session: "session-a"}, + suSe: mustNewSuSe(t, "", "session-a"), mode: LogoutModeSession, store: func(t *testing.T) store.Store { s := mocks.NewStore(t) @@ -214,7 +264,7 @@ func TestGetLogoutRecords(t *testing.T) { wantErrs: []error{ErrSuspiciousCacheResult}}, { name: "fails if the subject does not match the retrieved record", - suSe: SuSe{Subject: "subject-a"}, + suSe: mustNewSuSe(t, "subject-a", ""), mode: LogoutModeSubject, store: func(t *testing.T) store.Store { s := mocks.NewStore(t) @@ -228,7 +278,7 @@ func TestGetLogoutRecords(t *testing.T) { // key variation tests { name: "key variation: 'session-a'", - suSe: SuSe{Session: "session-a"}, + suSe: mustNewSuSe(t, "", "session-a"), mode: LogoutModeSession, store: func(*testing.T) store.Store { return sessionStore @@ -237,7 +287,7 @@ func TestGetLogoutRecords(t *testing.T) { }, { name: "key variation: 'session-b'", - suSe: SuSe{Session: "session-b"}, + suSe: mustNewSuSe(t, "", "session-b"), mode: LogoutModeSession, store: func(*testing.T) store.Store { return sessionStore @@ -246,7 +296,7 @@ func TestGetLogoutRecords(t *testing.T) { }, { name: "key variation: 'session-c'", - suSe: SuSe{Session: "session-c"}, + suSe: mustNewSuSe(t, "", "session-c"), mode: LogoutModeSession, store: func(*testing.T) store.Store { return sessionStore @@ -255,7 +305,7 @@ func TestGetLogoutRecords(t *testing.T) { }, { name: "key variation: 'ession-c'", - suSe: SuSe{Session: "ession-c"}, + suSe: mustNewSuSe(t, "", "ession-c"), mode: LogoutModeSession, store: func(*testing.T) store.Store { return sessionStore @@ -265,7 +315,7 @@ func TestGetLogoutRecords(t *testing.T) { }, { name: "key variation: 'subject-a'", - suSe: SuSe{Subject: "subject-a"}, + suSe: mustNewSuSe(t, "subject-a", ""), mode: LogoutModeSubject, store: func(*testing.T) store.Store { return sessionStore @@ -274,7 +324,7 @@ func TestGetLogoutRecords(t *testing.T) { }, { name: "key variation: 'subject-'", - suSe: SuSe{Subject: "subject-"}, + suSe: mustNewSuSe(t, "subject-", ""), mode: LogoutModeSubject, store: func(*testing.T) store.Store { return sessionStore