Skip to content
/ scs Public
forked from alexedwards/scs

HTTP Session Management for Go

License

Notifications You must be signed in to change notification settings

aberlorn/scs

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SCS - The Echo-Enabled Version

The aberlorn/scs fork of alexedwards/scs is built for Echo.

As appropriate we will keep this fork current with scs.

Unfortunately forked versions cannot post issues.

SCS: HTTP Session Management for Go

Installation

$ go get github.com/alexedwards/scs/v2

The Basics

SCS implements a session management pattern following the OWASP security guidelines. Session data is stored on the server, and a randomly-generated unique session token (or session ID) is communicated to and from the client in a session cookie.

package main

import (
	"io"
	"net/http"

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

var session *scs.Session

func main() {
	// Initialize the session manager.
	session = scs.NewSession()

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

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

func putHandler(w http.ResponseWriter, r *http.Request) {
	// Store a new key and value in the session data.
	session.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 := session.GetString(r.Context(), "message")
	io.WriteString(w, msg)
}
$ curl -i --cookie-jar cj --cookie cj localhost:4000/put
HTTP/1.1 200 OK
Cache-Control: no-cache="Set-Cookie"
Set-Cookie: session=lHqcPNiQp_5diPxumzOklsSdE-MJ7zyU6kjch1Ee0UM; Path=/; Expires=Sat, 27 Apr 2019 10:28:20 GMT; Max-Age=86400; HttpOnly; SameSite=Lax
Vary: Cookie
Date: Fri, 26 Apr 2019 10:28:19 GMT
Content-Length: 0

$ curl -i --cookie-jar cj --cookie cj localhost:4000/get
HTTP/1.1 200 OK
Date: Fri, 26 Apr 2019 10:28:24 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

Hello from a session!

Configuring Session Behavior

Session behavior can be configured via the Session fields. For example:

session = scs.NewSession()
session.Lifetime = 3 * time.Hour
session.IdleTimeout = 20 * time.Minute
session.Cookie.Persist = false
session.Cookie.SameSite = http.SameSiteStrictMode
session.Cookie.Secure = true

Documentation for all available settings and their default values can be found here.

Working with Session Data

Data can be set using the Put() method and retrieved with the Get() method. A variety of helper methods like GetString(), GetInt() and GetBytes() are included for common data types. Please see the documentation for a full list of helper methods.

The Pop() method (and accompanying helpers for common data types) act like a one-time Get(), retrieving the data and removing it from the session in one step. These are useful if you want to implement 'flash' message functionality in your application, where messages are displayed to the user once only.

Some other useful functions are Exists() (which returns a bool indicating whether or not a given key exists in the session data) and Keys() (which returns a sorted slice of keys in the session data).

Individual data items can be deleted from the session using the Remove() method. Alternatively, all session data can de deleted by using the Destroy() method. After calling Destroy(), any further operations in the same request cycle will result in a new session being created --- with a new session token and a new lifetime.

Loading and Saving Sessions

Most applications will use the LoadAndSave() middleware. This middleware takes care of loading and committing session data to the session store, and communicating the session token to/from the client in a cookie as necessary.

If you want to communicate the session token to/from the client in a different way (for example in a different HTTP header) you are encouraged to create your own alternative middleware using the code in LoadAndSave() as a template. An example is given here.

Or for more fine-grained control you can load and save sessions within your individual handlers (or from anywhere in your application). See here for an example.

Configuring the Session Store

By default SCS uses an in-memory store for session data. This is convenient (no setup!) and very fast, but all session data will be lost when your application is stopped or restarted. Therefore it's useful for applications where data loss is an acceptable trade off for fast performance, or for prototyping and testing purposes. In most production applications you will want to use a persistent session store like PostgreSQL or MySQL instead.

The session stores currently included are shown in the table below.

Package
memstore In-memory session store (default)
mysqlstore MySQL based session store
postgresstore PostgreSQL based session store
redisstore Redis based session store

Custom session stores are also supported. Please see here for more information.

Using with PostgreSQL

Please see the postgresstore package documentation for full information and sample code. But in summary...

You'll need to create a sessions table:

CREATE TABLE sessions (
	token TEXT PRIMARY KEY,
	data BYTEA NOT NULL,
	expiry TIMESTAMPTZ NOT NULL
);

CREATE INDEX sessions_expiry_idx ON sessions (expiry);

And then you can then use it like this:

package main

import (
	"database/sql"
	"io"
	"log"
	"net/http"

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

	_ "github.com/lib/pq"
)

var session *scs.Session

func main() {
	db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Initialize a new session manager and configure it to use PostgreSQL as
	// the session store.
	session = scs.NewSession()
	session.Store = postgresstore.New(db)

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

	http.ListenAndServe(":4000", session.LoadAndSave(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	session.Put(r.Context(), "message", "Hello from a session!")
}

func getHandler(w http.ResponseWriter, r *http.Request) {
	msg := session.GetString(r.Context(), "message")
	io.WriteString(w, msg)
}

A background 'cleanup' goroutine is automatically run to delete expired session data. This stops the database table from holding on to invalid sessions indefinitely and growing unnecessarily large. By default the cleanup will run every 5 minutes.

Using with MySQL

Please see the mysqlstore package documentation for full information and sample code. But in summary...

You'll need to create a sessions table:

CREATE TABLE sessions (
	token CHAR(43) PRIMARY KEY,
	data BLOB NOT NULL,
	expiry TIMESTAMP(6) NOT NULL
);

CREATE INDEX sessions_expiry_idx ON sessions (expiry);

And then you can then use it like this:

package main

import (
	"database/sql"
	"io"
	"log"
	"net/http"

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

	_ "github.com/go-sql-driver/mysql"
)

var session *scs.Session

func main() {
	db, err := sql.Open("mysql", "user:pass@/db?parseTime=true")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Initialize a new session manager and configure it to use PostgreSQL as
	// the session store.
	session = scs.NewSession()
	session.Store = mysqlstore.New(db)

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

	http.ListenAndServe(":4000", session.LoadAndSave(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	session.Put(r.Context(), "message", "Hello from a session!")
}

func getHandler(w http.ResponseWriter, r *http.Request) {
	msg := session.GetString(r.Context(), "message")
	io.WriteString(w, msg)
}

A background 'cleanup' goroutine is automatically run to delete expired session data. This stops the database table from holding on to invalid sessions indefinitely and growing unnecessarily large. By default the cleanup will run every 5 minutes.

Using with Redis

Please see the redisstore package documentation for full information and sample code. But in summary...

package main

import (
	"io"
	"net/http"

	"github.com/alexedwards/scs/v2"
	"github.com/alexedwards/scs/redisstore"
	"github.com/gomodule/redigo/redis"
)

var session *scs.Session

func main() {
	// Establish a redigo connection pool.
	pool := &redis.Pool{
		MaxIdle: 10,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", "localhost:6379")
		},
	}

	// Initialize a new session manager and configure it to use redisstore as
	// the session store.
	session = scs.NewSession()
	session.Store = redisstore.New(pool)

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

	http.ListenAndServe(":4000", session.LoadAndSave(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	session.Put(r.Context(), "message", "Hello from a session!")
}

func getHandler(w http.ResponseWriter, r *http.Request) {
	msg := session.GetString(r.Context(), "message")
	io.WriteString(w, msg)
}

Using Custom Session Stores

scs.Store defines the interface for custom session stores. Any object that implements this interface can be set as the store when configuring the session.

type Store interface {
	// Delete should remove the session token and corresponding data from the
	// session store. If the token does not exist then Delete should be a no-op
	// and return nil (not an error).
	Delete(token string) (err error)

	// Find should return the data for a session token from the store. If the
	// session token is not found or is expired, the found return value should
	// be false (and the err return value should be nil). Similarly, tampered
	// or malformed tokens should result in a found return value of false and a
	// nil err value. The err return value should be used for system errors only.
	Find(token string) (b []byte, found bool, err error)

	// Commit should add the session token and data to the store, with the given
	// expiry time. If the session token already exists, then the data and
	// expiry time should be overwritten.
	Commit(token string, b []byte, expiry time.Time) (err error)
}

Preventing Session Fixation

To help prevent session fixation attacks you should renew the session token after any privilege level change. Commonly, this means that the session token must to be changed when a user logs in or out of your application. You can do this using the RenewToken() method like so:

func loginHandler(w http.ResponseWriter, r *http.Request) {
	userID := 123

	// First renew the session token...
	err := session.RenewToken(r.Context())
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	// Then make the privilege-level change.
	session.Put(r.Context(), "userID", userID)
}

Multiple Sessions per Request

It is possible for an application to support multiple sessions per request, with different lifetime lengths and even different stores. Please see here for an example.

Compatibility

This package requires Go 1.11 or newer.

It is not compatible with the Echo framework. Please consider using the Echo session manager instead.

About

HTTP Session Management for Go

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Go 100.0%