Usage in project

I was learning how to maintain sessions in Go and went across the scs package for session middleware.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// In main.go
// available to whole main package
var session *scs.SessionManager

func main() {
app.InProduction = false
session = scs.New()
session.Lifetime = 24 * time.Hour
session.Cookie.Persist = true
session.Cookie.SameSite = http.SameSiteLaxMode
session.Cookie.Secure = app.InProduction
// ...

srv := &http.Server{
Addr: addr,
Handler: routes(&app),
}
}
1
2
3
4
5
6
// In middleware.go

// SessionLoad loads and saves the session on every request
func SessionLoad(next http.Handler) http.Handler{
return session.LoadAndSave(next)
}
1
2
3
4
5
6
7
8
9
10
In routes.go

func routes(app *config.AppConfig) http.Handler{
mux := chi.NewRouter()
// ...
mux.Use(SessionLoad)
mux.Get("/", handlers.Repo.ServeHome)
mux.Get("/about", handlers.Repo.ServeAbout)
return mux
}

Go through basic usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"io"
"net/http"
"time"

"github.com/alexedwards/scs/v2"
)

// Declare sessionManager here and make it available to whole main package
var sessionManager *scs.SessionManager

func main() {
// Initialize a new session manager and configure the session lifetime.
sessionManager = scs.New()
sessionManager.Lifetime = 24 * time.Hour

// mux is used for routing
mux := http.NewServeMux()
mux.HandleFunc("/put", putHandler)
mux.HandleFunc("/get", getHandler)

// Wrap your handlers with the LoadAndSave() middleware.
http.ListenAndServe(":4000", sessionManager.LoadAndSave(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
// Store a new key and value in the session data.
sessionManager.Put(r.Context(), "message", "Hello from a session!")
}

func getHandler(w http.ResponseWriter, r *http.Request) {
// Use the GetString helper to retrieve the string value associated with a
// key. The zero value is returned if the key does not exist.
msg := sessionManager.GetString(r.Context(), "message")
io.WriteString(w, msg)
}

Understanding Load function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Load retrieves the session data for the given token from the session store,
// and returns a new context.Context containing the session data. If no matching
// token is found then this will create a new session.
//
// Most applications will use the LoadAndSave() middleware and will not need to
// use this method.
func (s *SessionManager) Load(ctx context.Context, token string) (context.Context, error) {

// A Context carries a deadline, a cancellation signal, and other request-scope values across API boundaries.
// Context's methods may be called by multiple goroutines simultaneously.

// contextKey is the key used to set and retrieve the session data from a context.Context. It's automatically generated to ensure uniqueness.

// .(*sessionData) is a type assertion, similar to the 'as' keyword in C#
// If the session is already in the context, just return the context and nothing needs to be done.
if _, ok := ctx.Value(s.contextKey).(*sessionData); ok {
return ctx, nil
}

// If the token is empty, simply add a new, empty session data to the context.
if token == "" {
return s.addSessionDataToContext(ctx, newSessionData(s.Lifetime)), nil
}

// Find should return the data for a session token from the store, in byte array format
b, found, err := s.Store.Find(token)
if err != nil {
return nil, err
} else if !found {
// If no session data with the given token is found in the store, add empty session data to the context.
return s.addSessionDataToContext(ctx, newSessionData(s.Lifetime)), nil
}

sd := &sessionData{
status: Unmodified,
token: token,
}
// Decode the byte array found from the store
if sd.deadline, sd.values, err = s.Codec.Decode(b); err != nil {
return nil, err
}

// Mark the session data as modified if an idle timeout is being used. This
// will force the session data to be re-committed to the session store with
// a new expiry time.
if s.IdleTimeout > 0 {
sd.status = Modified
}

return s.addSessionDataToContext(ctx, sd), nil
}

Understanding LoadAndSave function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// LoadAndSave provides middleware which automatically loads and saves session
// data for the current request, and communicates the session token to and from
// the client in a cookie.
func (s *SessionManager) LoadAndSave(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var token string
cookie, err := r.Cookie(s.Cookie.Name)
if err == nil {
token = cookie.Value
}

ctx, err := s.Load(r.Context(), token)
if err != nil {
s.ErrorFunc(w, r, err)
return
}

sr := r.WithContext(ctx)
bw := &bufferedResponseWriter{ResponseWriter: w}
next.ServeHTTP(bw, sr)

if sr.MultipartForm != nil {
sr.MultipartForm.RemoveAll()
}

if s.Status(ctx) != Unmodified {
responseCookie := &http.Cookie{
Name: s.Cookie.Name,
Path: s.Cookie.Path,
Domain: s.Cookie.Domain,
Secure: s.Cookie.Secure,
HttpOnly: s.Cookie.HttpOnly,
SameSite: s.Cookie.SameSite,
}

switch s.Status(ctx) {
case Modified:
token, expiry, err := s.Commit(ctx)
if err != nil {
s.ErrorFunc(w, r, err)
return
}

responseCookie.Value = token

if s.Cookie.Persist || s.GetBool(ctx, "__rememberMe") {
responseCookie.Expires = time.Unix(expiry.Unix()+1, 0) // Round up to the nearest second.
responseCookie.MaxAge = int(time.Until(expiry).Seconds() + 1) // Round up to the nearest second.
}
case Destroyed:
responseCookie.Expires = time.Unix(1, 0)
responseCookie.MaxAge = -1
}

w.Header().Add("Set-Cookie", responseCookie.String())
addHeaderIfMissing(w, "Cache-Control", `no-cache="Set-Cookie"`)
addHeaderIfMissing(w, "Vary", "Cookie")
}

if bw.code != 0 {
w.WriteHeader(bw.code)
}
w.Write(bw.buf.Bytes())
})
}