mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-26 03:57:27 -05:00
fix: use base64 record keys to prevent separator clashes with subjects or sessionIds that contain a dot
This commit is contained in:
committed by
Christian Richter
parent
910298aa05
commit
fd614eacf1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user