sessions

package module
v0.0.0-...-8a3fdd9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2022 License: MIT Imports: 18 Imported by: 2

README

Godoc Reference Go Report

This Go package attempts to free you from the hard work of implementing safe cookie-based web sessions.

Sessions implements a number of OWASP recommendations:

  • No data storage on the client
  • Automatic session expiry
  • Session ID regeneration
  • Anomaly detection via IP address and user agent analysis

Additional features:

  • Session key/value storage
  • Log in/out functions for users
  • Various identifier generation functions
  • Password strength checks (based on NIST recommendations)
  • Lots of configuration options
  • Database-agnostic, choose your own backend
  • It's not a framework, everything is based on net/http.
  • Extensive documentation

If you want to go one step further and have user signup, login, logout, password reset, email/password change implemented for you, check out github.com/rivo/users.

Installation

go get github.com/rivo/sessions

Simple Example

func MyHandler(response http.ResponseWriter, request *http.Request) {
  session, err := sessions.Start(response, request, false)
  if err != nil {
    panic(err)
  }
  if session != nil {
    fmt.Println("We have a session")
  } else {
    fmt.Println("We have no session")
  }
}

(Providing true will always return a session.)

With the session object, you can call:

  • RegenerateID to switch the session ID,
  • Set, Get, GetAndDelete, and Delete to (un-)assign values to keys,
  • LogIn and LogOut to attach/detach users,
  • GobEncode, GobDecode, MarshalJSON, and UnmarshalJSON to (un-)serialize sessions,
  • Destroy to end a session.

Configuration Options

  • SessionCookie: Name of the session cookie.
  • NewSessionCookie: Function for new cookies (used to set cookie parameters).
  • SessionExpiry: Time to expiry for inactive sessions.
  • SessionIDExpiry: Maximum session ID lifetime before automatic regeneration.
  • SessionIDGracePeriod: Extended lifetime for regenerated session IDs.
  • AcceptRemoteIP: Accepted level of change for IP addresses.
  • AcceptChangingUserAgent: Whether or not user agent changes are accepted.
  • MaxSessionCacheSize: Size of local (write-through) session cache.
  • SessionCacheExpiry: Maximum session lifetime in local cache.

Then there is Persistence used to connect to the session store of your choice (defaults to RAM).

Documentation

See http://godoc.org/github.com/rivo/sessions for the documentation.

See also the Wiki for more examples and explanations.

Your Feedback

Add your issue here on GitHub. Feel free to get in touch if you have any questions.

Release Notes

  • v0.1 (2017-11-11)
    • First release.

Documentation

Overview

Package sessions provides tools to manage cookie-based web sessions. Special emphasis is placed on security by implementing OWASP recommendations, specifically the following features:

  • No data storage on the client
  • Automatic session expiry
  • Session ID regeneration
  • Anomaly detection via IP address and user agent analysis

In addition, the package provides the following functionality:

  • Session key/value storage
  • Log in/out functions for users
  • Various identifier generation functions
  • Password strength checks (based on NIST recommendations)

While simple to use, the package offers a number of extensively documented configuration variables. It also does not assume specific backend technologies. That is, any session storage system may be used simply by implementing the PersistenceLayer interface (or parts of it).

This package is currently not written to be run on multiple machines in a distributed fashion without a load balancer that implements sticky sessions. This may change in the future.

Basic Example

Although some more configuration needs to happen for production readiness, the package's defaults allow you to get started very quickly. To get access to the current session, simply call Start():

func MyHandler(response http.ResponseWriter, request *http.Request) {
	session, err := sessions.Start(response, request, false)
	if err != nil {
		panic(err)
	}
	if session != nil {
		fmt.Println("We have a session")
	} else {
		fmt.Println("We have no session")
	}
}

By providing "true" instead of "false" to the Start() function, you can force the creation of a session, even if there previously was none.

Once you have a session, you can identify a user across multiple HTTP requests. You may add values to the session, attach a user to it, cause its session ID to change, or destroy it again. For more extensive user-centered functions (for example, signing up, logging in and out, changing passwords etc.), see the subdirectory "users".

Configuration

Before putting your application into production, you must implement the NewSessionCookie function:

NewSessionCookie = func() *http.Cookie {
  return &http.Cookie{
    Expires:  time.Now().Add(10 * 365 * 24 * time.Hour),
    MaxAge:   10 * 365 * 24 * 60 * 60,
    HttpOnly: true,
    Domain:   "www.example.com",
    Path:     "/",
    Secure:   true,
  }
}

You may choose a different expiry date, domain, and path but the other fields are mandatory (given that you are using TLS which you certainly should).

You can change the name of the cookie by changing the SessionCookie variable. The default is the inconspicuous string "id".

The following timeout values may be adjusted according to the requirements of your application:

  • SessionExpiry: The maximum time which may pass before a session that has not been accessed will be destroyed. The default is "forever", meaning unused sessions will not time out.
  • SessionIDExpiry: The maximum duration a session ID can be used before it is changed to a new session ID. Session ID renewals reduce the risk of session hijacking attacks.
  • SessionIDGracePeriod: Session ID renewals require the previous session ID to remain active for some time so sessions don't get lost, e.g. because of a slow network. This variable specifies how long a previous session ID remains active when a new session ID is already in place.

To further reduce the risk of session hijacking attacks, this package checks client IP addresses as well as user agent strings and destroys sessions if changes in these properties were detected. Refer to the AcceptRemoteIP and AcceptChangingUserAgent variables for more information.

The Session Cache and the Persistence Layer

Sessions are stored in a local RAM cache (which is a simpe map) whose size is defined by the MaxSessionCacheSize variable. If you set this variable to 0, no sessions are held locally. The SessionCacheExpiry controls when a session will be purged from the cache based on the last time it was used.

The cache is write-through (except for session last access times). That is, every time a change was made to a session, that change is forwarded to the package's persistence layer to be saved. The persistence layer is a collection of functions which allow the storage and retrieval of objects from a permanent data store. For example, you may use an SQL database or a key-value store.

See the documentation of PersistenceLayer for details on the functions to be implemented. If you need to implement only some of the functions, you may use ExtendablePersistenceLayer instead of creating your own class. The package default is to do nothing. That is, sessions are not persisted and therefore will get lost when purged from the local cache or when the application exits.

Session objects implement gob.GobEncoder/gob.GobDecoder and json.Marshaler/json.Unmarshaler. While encoding to JSON allows you to easily inspect session attributes in your database, GOB serialization is preferred as it will restore session objects precisely. (For example, the JSON package always unmarshals numbers into floats even if they were originally integers.)

It is recommended that you purge your data store from expired sessions from time to time, e.g. by using a cron job, because users may abandon your website which will leave old sessions in your store.

It is recommended to call PurgeSessions() before exiting the program. This will cause session last access times to be updated.

Utility Functions

This package provides a number of utility functions which may be useful in the context of session and user management.

The CUID() function generates Base-62 "compact unique identifiers" suitable for user IDs.

The RandomID() function generates random Base-62 strings of any length.

The ReasonablePassword() function checks the strength of a password based on the recommendations of NIST SP 800-63B.

Index

Constants

View Source
const (
	PasswordOK                = iota // Password passes our rules.
	PasswordTooShort                 // Password is too short.
	PasswordIsAName                  // Password is one of the predefined names.
	PasswordWasCompromised           // Password was found in a list of compromised passwords.
	PasswordFoundInDictionary        // Password was found in a dictionary.
	PasswordRepetitive               // Password consists of just repetetive characters.
	PasswordSequential               // Password consists of a simple sequence.
)

Constants for password problems returned with AnalyzePassword().

Variables

View Source
var (
	// Persistence provides the methods which read/write information from/to an
	// external (permanent) data store.
	Persistence PersistenceLayer = ExtendablePersistenceLayer{}

	// SessionExpiry is the maximum time which may pass before a session that
	// has not been accessed will be destroyed, hence logging a user out.
	SessionExpiry time.Duration = math.MaxInt64

	// SessionIDExpiry is the maximum duration a session ID can be used before it
	// is changed to a new session ID. This helps prevent session hijacking. It
	// may be set to 0, leading to a session ID change with every request.
	// However, this will increase the load on the session persistence layer
	// considerably.
	//
	// Note that expired session IDs will remain active for the duration of
	// SessionIDGracePeriod (leading to session ID overlaps) to avoid race
	// conditions when multiple requests are issued at nearly the same time.
	SessionIDExpiry = time.Hour

	// SessionIDGracePeriod is the duration for a replaced (old) session ID to
	// remain active so multiple concurrent requests from the browser don't
	// accidentally lead to session loss. While the default of five minutes may
	// appear long, in a mobile context or other slow networks, it is a reasonable
	// time.
	SessionIDGracePeriod = 5 * time.Minute

	// AcceptRemoteIP determines how much change of an IPv4 remote IP address is
	// accepted before destroying a session. If set to 4, the last (4th) byte of
	// the client's IP address may change but if the 3rd byte changes compared to
	// the last request, the session is destroyed. And so on. A value of 1 means
	// that any changes in the client's IP address are accepted.
	//
	// When dealing with very sensitive data, it is suggested to set this value
	// to 4 so that when the user connects from a different network, they will be
	// required to log in again. Session hijacking becomes much more difficult
	// that way.
	//
	// IPv6 address or ports, while stored, are currently disregarded.
	//
	// Note that this does not work if your server runs behind a proxy.
	AcceptRemoteIP = 1

	// AcceptChangingUserAgent determines if the remote browser's user agent is
	// checked for consistency. We assume that the user agent for the current
	// session will always remain the same. If it changes, the session is
	// destroyed.
	//
	// By setting this value to "true", sessions will be kept alive even if the
	// user agent string changes.
	AcceptChangingUserAgent = false

	// SessionCookie is the name of the session cookie that will contain the
	// session ID.
	SessionCookie = "id"

	// NewSessionCookie is used to create new session cookies or to renew them.
	// The "Name" and "Value" fields need not be set. It is recommended that you
	// overwrite the default implementation with your specific defaults,
	// especially the "Domain", "Path", and "Secure" fields. Be sure to set
	// "Secure" to true when using TLS (HTTPS). For more information on cookies,
	// refer to:
	//
	//     - https://tools.ietf.org/html/rfc6265
	//     - https://en.wikipedia.org/wiki/HTTP_cookie#Cookie_attributes
	NewSessionCookie = func() *http.Cookie {
		return &http.Cookie{
			Expires:  time.Now().Add(10 * 365 * 24 * time.Hour),
			MaxAge:   10 * 365 * 24 * 60 * 60,
			HttpOnly: true,
		}
	}

	// MaxSessionCacheSize is the maximum size of the local sessions cache. If
	// this value is 0, nothing is cached. If this value is negative, the cache
	// may expand indefinitely. When the maximum size is reached, sessions with
	// the oldest access time are discarded. They are also removed from the cache
	// when their age exceeds SessionCacheExpiry. (This is checked whenever the
	// cache is accessed.)
	//
	// Set this value to 0 if you want to rely on a different cache library. Then
	// connect it via the persistence layer.
	MaxSessionCacheSize = 1024 * 1024

	// SessionCacheExpiry is the maximum duration an inactive session will remain
	// in the local cache.
	SessionCacheExpiry = time.Hour
)

Functions

func CUID

func CUID() string

CUID returns a compact unique identifier suitable for user IDs. The goal is to minimize collisions while keeping the identifier short. The returned identifiers are exactly 11 bytes long, consisting of letters and numbers (Base62). They are generated from a 64-bit value with the following fields:

  • Bit 64-25: A timestamp. The number of milliseconds since Jan 1, 2017, omitting all bits above bit 40. Timestamps start over about every 34 years. Thus, within these time periods, user IDs should be sortable lexicographically.
  • Bit 24-9: A 16-bit hash of this computer's MAC address.
  • Bit 8-1: A counter which increases with every consecutive call to this function which results in the same timestamp. Bits 8 and above, if any, will spill into the MAC address's hash.

To generate IDs for non-user data, you may refer to other libraries such as https://github.com/segmentio/ksuid.

func LogOut

func LogOut(userID interface{}) error

LogOut logs the user with the given ID out of all sessions. This requires that Persistence.UserSessions() be implemented, returning all IDs of sessions that contain this user.

func PurgeSessions

func PurgeSessions()

PurgeSessions removes all sessions from the local cache. The current cache content is also saved via the persistence layer, to update the session last access times.

func RandomID

func RandomID(length int) (string, error)

RandomID returns a random Base62-encoded string with the given length. To avoid collisions, use a length of at least 22 (which corresponds to a minimum of 128 bits).

func ReasonablePassword

func ReasonablePassword(password string, names []string) int

ReasonablePassword checks the strength of a password and returns one of the password constants as a result (PasswordOK if no major issues were found).

The tests performed by this function follow the NIST SP 800-63B guidelines (section 5.1.1), with two modifications: The list of compromised passwords has been shortened to the top 100,000 and we're using an english dictionary only so far.

func RefreshUser

func RefreshUser(user User) error

RefreshUser gets all sessions for the given user and updates their user object. This should be done when the user object has changed (e.g. a password change). It ensures that all sessions of a user have the same user object. This requires that Persistence.UserSessions() be implemented, returning all IDs of sessions that contain this user.

Calling this function is not necessary if you don't use the local cache (i.e. MaxSessionCacheSize is 0) and if serialized session objects only contain the user ID (as it is with the provided default serlization functions GobEncode() and MarshalJSON()).

Note that this call will fail if the user ID itself was changed. Such a change is more difficult and is not covered here.

Types

type ExtendablePersistenceLayer

type ExtendablePersistenceLayer struct {
	LoadSessionFunc   func(id string) (*Session, error)
	SaveSessionFunc   func(id string, session *Session) error
	DeleteSessionFunc func(id string) error
	UserSessionsFunc  func(userID interface{}) ([]string, error)
	LoadUserFunc      func(id interface{}) (User, error)
}

ExtendablePersistenceLayer implements the PersistenceLayer interface by doing nothing (or the absolute minimum) or, if one of the field functions are set, calling those instead.

Use this type if you only intend to use a small part of this package's functionality.

func (ExtendablePersistenceLayer) DeleteSession

func (p ExtendablePersistenceLayer) DeleteSession(id string) error

DeleteSession delegates to DeleteSessionFunc or does nothing.

func (ExtendablePersistenceLayer) LoadSession

func (p ExtendablePersistenceLayer) LoadSession(id string) (*Session, error)

LoadSession delegates to LoadSessionFunc or returns a nil session.

func (ExtendablePersistenceLayer) LoadUser

func (p ExtendablePersistenceLayer) LoadUser(id interface{}) (User, error)

LoadUser delegates to LoadUserFunc or returns a nil user.

func (ExtendablePersistenceLayer) SaveSession

func (p ExtendablePersistenceLayer) SaveSession(id string, session *Session) error

SaveSession delegates to SaveSessionFunc or does nothing.

func (ExtendablePersistenceLayer) UserSessions

func (p ExtendablePersistenceLayer) UserSessions(userID interface{}) ([]string, error)

UserSessions delegates to UserSessionsFunc or returns nil.

type PersistenceLayer

type PersistenceLayer interface {
	// LoadSession retrieves a session from the permanent data store and returns
	// it. If no session is found for the given ID, that's not an error. A nil
	// session should be returned in that case.
	//
	// Session stores are typically key-value databases. We can use encoding/gob
	// to unserialize sessions. For example, if your session store accepts only
	// string pairs, this is how we can load a Base64-encoded session:
	//
	//     func LoadSession(id string) (*Session, error) {
	//       // Load Base64-string s from database first...
	//       data, err := base64.StdEncoding.DecodeString(s)
	//       if err != nil {
	//         return nil, err
	//       }
	//       r := bytes.NewReader(data)
	//       decoder := gob.NewDecoder(r)
	//       var session Session
	//       if err := decoder.Decode(&session); err != nil {
	//         return nil, err
	//       }
	//       return &session, nil
	//     }
	//
	// Alternatively, the json.Unmarshaler interface may be used.
	//
	// When using the built-in decoders (gob or json) and a User was attached to
	// the session, LoadUser() is called implicitly with the stored user ID.
	LoadSession(id string) (*Session, error)

	// SaveSession saves a session to the permanent data store. If the store does
	// not contain the session yet, it is inserted. Otherwise, it is simply
	// updated. Session stores are typically key-value databases. We can use
	// encoding/gob to serialize sessions. For example, if your session store
	// accepts only string pairs, this is how we can save a Base64-encoded
	// session:
	//
	//     func SaveSession(id string, session *Session) error {
	//     	 var buffer bytes.Buffer
	//     	 encoder := gob.NewEncoder(&buffer)
	//     	 if err := encoder.Encode(session); err != nil {
	//     	 	return err
	//     	 }
	//     	 s := base64.StdEncoding.EncodeToString(buffer.Bytes())
	//     	 // Now save id + s to database.
	//     	 return nil
	//     }
	//
	// Alternatively, the json.Marshaler interface may be used. Note, however,
	// that while JSON serialization allows you to peek into the serialized data,
	// it may not convert values back the same as they were stored: Any numeric
	// values will convert back as float64 types, all slices will convert back
	// as []interface{}, and all maps will convert back as map[string]interface{}.
	//
	// The internal encoders (gob or json) do not save the full User object but
	// only the user ID.
	//
	// Session IDs are always Base64-encoded strings with a length of 24.
	//
	// The session object is locked while this function is called.
	SaveSession(id string, session *Session) error

	// DeleteSession deletes a session from the permanent data store. It is not
	// an error if the session ID does not exist.
	//
	// Note that this package only deletes expired sessions that are accessed. If
	// a session expires because e.g. the user does not come back, it will not
	// be deleted via this method. It is suggested that you periodically run a
	// cron job to purge sessions that have expired. Use session.Expired() for
	// this or, if you can access session data directly:
	//
	//   session.referenceID != "" &&
	//   time.Since(session.lastAccess) >= SessionIDGracePeriod ||
	//   time.Since(session.lastAccess) >= SessionExpiry &&
	//   time.Since(session.created) >= SessionIDExpiry+SessionIDGracePeriod
	DeleteSession(id string) error

	// UserSessions returns all session IDs of sessions which have the given user
	// (specified by their user ID) attached to them. This is only used to log
	// users out of all of their existing sessions. You may return nil, which will
	// allow users to be logged on with multiple different sessions at the same
	// time.
	UserSessions(userID interface{}) ([]string, error)

	// LoadUser loads the user with the given unqiue user ID (typically the
	// primary key) from the data store.
	LoadUser(id interface{}) (User, error)
}

PersistenceLayer provides the methods which read/write user information from/to the permanent data store.

type Session

type Session struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

Session represents a browser session which may persist across multiple HTTP requests. A session is usually generated with the Start() function and may be destroyed with the Destroy() function.

Sessions are uniquely identified by their session ID. This session ID is regenerated, i.e. exchanged, regularly to prevent others from hijacking sessions. This can be done explicitly with the RegenerateID() function. And it happens automatically based on the rules defined in this package (see package variables for details).

The functions for this type are thread-safe.

func Start

func Start(response http.ResponseWriter, request *http.Request, createIfNew bool) (*Session, error)

Start returns a session for the given HTTP request. Because this function may manipulate browser cookies, it must be called before any text is written to the response writer.

Sessions are returned from the local cache if contained or loaded into the cache first if not.

A nil value may also be returned if "createIfNew" is false and no session was previously assigned to this user. Note that if the user's browser rejects cookies, this will cause a new session to be created with every request. You will also want to respect any privacy laws regarding the use of cookies, user and session data.

The following package variables influence the session handling (see their comments for details):

  • SessionExpiry
  • SessionIDExpiry
  • SessionCookie
  • NewSessionCookie

func (*Session) Delete

func (s *Session) Delete(key string) error

Delete deletes a key from the session. Note that since the sessions cache is write-through, this will also result in a call to SaveSession() of the persistence layer. The error returned is the error from SaveSession().

func (*Session) Destroy

func (s *Session) Destroy(response http.ResponseWriter, request *http.Request) error

Destroy marks the end of this session. It is deleted from the session cache, the persistence layer, and the user's browser cookie is marked as expired.

The session should not be used anymore after this call.

func (*Session) Expired

func (s *Session) Expired() bool

Expired returns whether or not this session has expired. This is useful to frequently purge the session store.

func (*Session) Get

func (s *Session) Get(key string, def interface{}) interface{}

Get returns a value stored in the session under the given key. If the key is not contained, the default "def" is returned.

func (*Session) GetAndDelete

func (s *Session) GetAndDelete(key string, def interface{}) interface{}

GetAndDelete returns a value stored in the session under the given key. If the key is not contained, the default "def" is returned. The key is also deleted from the session.

func (*Session) GobDecode

func (s *Session) GobDecode(from []byte) error

GobDecode unserializes a session from the given byte array.

func (*Session) GobEncode

func (s *Session) GobEncode() ([]byte, error)

GobEncode serializes a session to a byte array.

func (*Session) LastAccess

func (s *Session) LastAccess() time.Time

LastAccess returns the time this session was last accessed.

func (*Session) LogIn

func (s *Session) LogIn(user User, exclusive bool, response http.ResponseWriter) error

LogIn assigns a user to this session, replacing any previously assigned user. If "exclusive" is set to true, all other sessions of this user will be deleted, effectively logging them out of any existing sessions first. This requires that Persistence.UserSessions() returns all of a user's sessions.

A call to this function also causes a session ID change for security reasons. It must be called before any non-header content is sent to the browser.

func (*Session) LogOut

func (s *Session) LogOut() error

LogOut logs the currently logged in user out of this session.

Note that the session will still be alive. If you want to destroy the current session, too, call Destroy() afterwards.

If no user is logged into this session, nothing happens.

func (*Session) MarshalJSON

func (s *Session) MarshalJSON() ([]byte, error)

MarshalJSON serializes the session into JSON.

func (*Session) RegenerateID

func (s *Session) RegenerateID(response http.ResponseWriter) error

RegenerateID generates a new session ID and replaces it in the current session. Use this every time there is a change in user privilege level or a related change, e.g. when the user access rights change or when their password was changed.

To avoid losing sessions when the network is slow or when many requests for the same session ID come in at the same time, the old session (with the old key) is turned into a reference session which will be valid for a grace period (defined in SessionIDGracePeriod). When that reference session is requested, the new session will be returned in its place.

func (*Session) Set

func (s *Session) Set(key string, value interface{}) error

Set stores a value under a key in the session which can then be retrieved with Get(). Any previous value stored under the same key will be overwritten. Note that since the sessions cache is write-through, this will also result in a call to SaveSession() of the persistence layer. The error returned is the error from SaveSession().

func (*Session) UnmarshalJSON

func (s *Session) UnmarshalJSON(data []byte) error

UnmarshalJSON unserializes a JSON string into a session.

func (*Session) User

func (s *Session) User() User

User returns the user for this session or nil if no user is attached to it, i.e. if the user is logged out. When checking for nil, it is not enough to just check for a nil (User) interface. You may also need to cast the interface to your own user type and check if it is nil.

type User

type User interface {
	// GetID returns the user's unique ID.
	GetID() interface{}
}

User represents one person who has access to the system.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL