github.com/decred/politeia@v1.4.0/politeiawww/sessions/sessions.go (about)

     1  // Copyright (c) 2021-2022 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package sessions
     6  
     7  import (
     8  	"encoding/base32"
     9  	"errors"
    10  	"net/http"
    11  	"strings"
    12  
    13  	"github.com/gorilla/securecookie"
    14  	"github.com/gorilla/sessions"
    15  )
    16  
    17  var (
    18  	_ sessions.Store = (*sessionStore)(nil)
    19  )
    20  
    21  // sessionStore is a custom sessions store that implements the gorilla/sessions
    22  // Store interface.
    23  type sessionStore struct {
    24  	Codecs  []securecookie.Codec
    25  	Options *sessions.Options
    26  	db      DB
    27  }
    28  
    29  // NewOptions returns a Options for the session store that is configured
    30  // conservatively. Only deviate from this configuration if you know what
    31  // you're doing.
    32  //
    33  // sessionMaxAge should be given in seconds. The session store prevents
    34  // session values from being returned once a session expires.
    35  func NewOptions(sessionMaxAge int) *sessions.Options {
    36  	return &sessions.Options{
    37  		Path:     "/",
    38  		MaxAge:   sessionMaxAge,
    39  		Secure:   true,
    40  		HttpOnly: true,
    41  		SameSite: http.SameSiteStrictMode,
    42  	}
    43  }
    44  
    45  // NewStore returns a new sessionStore.
    46  //
    47  // Keys are defined in pairs to allow key rotation, but the common case is
    48  // to set a single authentication key and optionally an encryption key.
    49  //
    50  // The first key in a pair is used for authentication and the second for
    51  // encryption. The encryption key can be set to nil or omitted in the last
    52  // pair, but the authentication key is required in all pairs.
    53  //
    54  // It is recommended to use an authentication key with 32 or 64 bytes.
    55  // The encryption key, if set, must be either 16, 24, or 32 bytes to select
    56  // AES-128, AES-192, or AES-256 modes.
    57  func NewStore(db DB, opts *sessions.Options, keyPairs ...[]byte) *sessionStore {
    58  	// Set default options if none were provided
    59  	if opts == nil {
    60  		opts = NewOptions(0)
    61  	}
    62  
    63  	// Set the maxAge for each securecookie instance
    64  	codecs := securecookie.CodecsFromPairs(keyPairs...)
    65  	for _, codec := range codecs {
    66  		if sc, ok := codec.(*securecookie.SecureCookie); ok {
    67  			sc.MaxAge(opts.MaxAge)
    68  		}
    69  	}
    70  
    71  	return &sessionStore{
    72  		Codecs:  codecs,
    73  		Options: opts,
    74  		db:      db,
    75  	}
    76  }
    77  
    78  // New returns a session for the given name without adding it to the registry.
    79  //
    80  // The sessions Store interface dictates that New() should never return a nil
    81  // session, even in the case of an error if using the Registry infrastructure
    82  // to cache the session.
    83  //
    84  // The difference between New() and Get() is that calling New() twice will
    85  // decode the session data twice, while Get() registers and reuses the same
    86  // decoded session after the first call.
    87  //
    88  // This function satisfies the gorilla/sessions Store interface.
    89  func (s *sessionStore) New(r *http.Request, cookieName string) (*sessions.Session, error) {
    90  	log.Tracef("New %v", cookieName)
    91  
    92  	// Setup new session
    93  	session := sessions.NewSession(s, cookieName)
    94  	opts := *s.Options
    95  	session.Options = &opts
    96  	session.IsNew = true
    97  	session.ID = newSessionID()
    98  
    99  	// Check if the session cookie already exists
   100  	c, err := r.Cookie(cookieName)
   101  	if errors.Is(err, http.ErrNoCookie) {
   102  		log.Tracef("Session cookie not found; returning a new session")
   103  		return session, nil
   104  	} else if err != nil {
   105  		return session, err
   106  	}
   107  	if c.Value == "" {
   108  		log.Tracef("Empty session value; returning new session")
   109  		return session, nil
   110  	}
   111  
   112  	// Session cookie already exists. The encoded session ID travels in
   113  	// the cookie. Decode it and use it to check if the session exists
   114  	// in the store.
   115  
   116  	// Decode session ID (overwrites existing session ID)
   117  	err = securecookie.DecodeMulti(cookieName, c.Value,
   118  		&session.ID, s.Codecs...)
   119  	switch {
   120  	case err == nil:
   121  		// Expected; continue
   122  
   123  	case strings.Contains(err.Error(), "expired timestamp"):
   124  		// The session has expired. It's not possible anymore
   125  		// retrieve the encoded session ID from the session,
   126  		// which also means it's not possible to retrieve the
   127  		// encoded session values from the database. A new
   128  		// session with empty values is returned.
   129  		log.Tracef("Session expired; returning a new session")
   130  		return session, nil
   131  
   132  	default:
   133  		// If there are any issues decoding the session ID,
   134  		// the existing session is considered invalid and
   135  		// the newly created session is returned.
   136  		log.Errorf("Failed to decode session: %v", err)
   137  		log.Tracef("Session invalid; returning new session")
   138  		return session, nil
   139  	}
   140  
   141  	// Check if the session exists in the database
   142  	encodedSession, err := s.db.Get(session.ID)
   143  	switch err {
   144  	case nil:
   145  		// Sanity check. If this is hit then it means that the
   146  		// sessions database is not implemented correctly. The
   147  		// database MUST return a ErrNotFound if a session is
   148  		// not found.
   149  		if encodedSession == nil {
   150  			panic("database did not return a session or an error")
   151  		}
   152  
   153  		// The session was found in the database. Decode the
   154  		// session values into the session being returned.
   155  		session.IsNew = false
   156  		err = securecookie.DecodeMulti(session.Name(),
   157  			encodedSession.Values, &session.Values,
   158  			s.Codecs...)
   159  		if err != nil {
   160  			return session, err
   161  		}
   162  		log.Tracef("Session found %v", session.ID)
   163  
   164  	case ErrNotFound:
   165  		// Session not found in database; return the new one.
   166  		log.Tracef("Session not found; returning new session")
   167  
   168  	default:
   169  		// All other errors
   170  		return session, err
   171  	}
   172  
   173  	return session, nil
   174  }
   175  
   176  // Save saves the encoded session values to the database and the encoded
   177  // session ID to the http response cookie.
   178  //
   179  // If the Options.MaxAge of the session is <= 0 then the session will be
   180  // deleted from the database. With this process it enforces proper session
   181  // cookie handling so no need to trust in the cookie management in the web
   182  // browser.
   183  //
   184  // This function satisfies the gorrila/sessions Store interface.
   185  func (s *sessionStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
   186  	log.Tracef("Save %v", session.ID)
   187  
   188  	// Delete session if max-age is <= 0
   189  	if session.Options.MaxAge <= 0 {
   190  		err := s.db.Del(session.ID)
   191  		if err != nil {
   192  			return err
   193  		}
   194  		http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
   195  		return nil
   196  	}
   197  
   198  	// Encode session values
   199  	encodedValues, err := securecookie.EncodeMulti(session.Name(),
   200  		session.Values, s.Codecs...)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	// Save session to the store
   206  	err = s.db.Save(session.ID, EncodedSession{
   207  		Values: encodedValues,
   208  	})
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	// Update session cookie with the encoded session ID
   214  	encodedID, err := securecookie.EncodeMulti(session.Name(),
   215  		session.ID, s.Codecs...)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	c := sessions.NewCookie(session.Name(), encodedID, session.Options)
   220  	http.SetCookie(w, c)
   221  
   222  	return nil
   223  }
   224  
   225  // Get returns a session for the given name after adding it to the registry.
   226  //
   227  // A new session is returned if the given session doesn't exist. Access IsNew
   228  // on the session to check if it is an existing session or a new one. The new
   229  // session will not have any sessions values set and will not have been saved
   230  // to the session store yet.
   231  //
   232  // A nil session is never returned. If an error occurs, a new session and the
   233  // error will both be returned. This is the behavior that gorilla/sessions
   234  // expects.
   235  //
   236  // This function satisfies the gorilla/sessions Store interface.
   237  func (s *sessionStore) Get(r *http.Request, cookieName string) (*sessions.Session, error) {
   238  	log.Tracef("Get %v", cookieName)
   239  
   240  	return sessions.GetRegistry(r).Get(s, cookieName)
   241  }
   242  
   243  // newSessionID returns a new session ID. A session ID is defined as a 32 byte
   244  // base32 string with padding. The session ID is set by the store and can be
   245  // whatever the store chooses. This ID was chosen simply because it's what the
   246  // gorilla/sesssions package reference implemenation uses.
   247  func newSessionID() string {
   248  	return base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
   249  }