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 }