sessions

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2018 License: MIT Imports: 13 Imported by: 0

README

Go Sessions

Build Status GoDoc

A simple, modular sessions package for Go web services. Key features include:

  • Very simple to integrate and use
  • Modular design so you can use as much or as little as you wish
  • Session tokens are crypto-random and digitally-signed to prevent session hopping
  • Supports signing key rotation for added security
  • You define the struct for session state, so it remains type-safe in your own code
  • Session state can be stored in any database for which a Store implementation exists. Currently there is an implementation for redis, but others are welcome via a PR.

Installation

go get github.com/davestearns/sessions

Basic Usage

Initialization

Start by constructing a session Store in your startup code. For example, to create a new redis store, use code like this:

import (
    "time"
    "github.com/davestearns/sessions"
)

func main() {
    redisAddr := // ... network address of your redis server
    store := sessions.NewRedisStore(
        sessions.NewRedisPool(redisAddr, time.Minute*5), 
        time.Hour)
}

The NewRedisPool() function creates a new redis.Pool instance that is configured with defaults that should work well in most situations. The time duration passed as the second parameter controls when the pool will do a health check on the connection: if the connection has been idle for longer than the duration, the pool will execute a PING request to ensure that the connection is still alive.

The time duration passed as the second parameter to NewRedisStore() controls the time-to-live for session state. The TTL is reset each time you get the state, so this controls how long idle sessions will remain before expiring.

Next, construct a Manager and give it your token signing key(s), along with your store. The keys are used to digitally sign the session tokens returned to clients, so that we can easily detect attempts to modify the token to session-hop. If you supply more than one key, the manager will rotate which key it uses, making it harder for an attacker to crack your signing key.

    //load your token signing keys from environment variables or wherever
    signingKeys := []string{os.GetEnv(SIGNKEY_1), os.GetEnv(SIGNKEY_2)}
    manager := sessions.NewManager(sessions.DefaultIDLength, signingKeys, store)
Beginning a Session

To begin a session within one of your handler functions, use manager.BeginSession():

func SignInHandler(w http.ResponseWriter, r *http.Request) {
    //...authentication code...

    //construct and initialize your own session state struct
    sessionState := NewSessionState(/* ... */)

    //begin a new session: this will add an Authorization header to the response
    //containing the new session token. The new token is returned in case you
    //want to do something with it.
    token, err := manager.BeginSession(w, sessionState)
    if err != nil {
        //...handle error...
    }

    //...write response body...
}

The .BeginSession() method will add an Authorization header to the response containing the value Bearer <token-string>. The <token-string> will be a base64-encoded version of the newly-generated session token. The session ID portion of the token is a series of crypto-random bytes, the length of which is controlled by the idLength parameter passed to sessions.NewManager. The token also contains an HMAC signature of the ID, which is generated using one of your signing keys.

Clients should hold on to this Authorization response header value and send it back to the server with all subsequent requests. The .GetState() method described below will extract the session token from the Authorization request header, verify it, and fetch the associated state from the store. If the client attempted to modify the token, the HMAC signature verification will fail, an the token will be considered invalid.

This package uses the Authorization header instead of a cookie to avoid CSRF attacks. Since Authorization headers are not handled automatically by the browser, they are not susceptible to typical CSRF attacks, but they do require some client-side JavaScript to receive the response header value, and include that value in the Authorization header on all subsequent requests.

For strategies on how you can share your global Manager instance with your handler functions see Sharing Values with Go Handlers.

Getting Session State

To get the previously-saved session state during subsequent requests, use manager.GetState():

func SomeStatefulHandler(w http.ResponseWriter, r *http.Request) {
    //create a new empty session state
    sessionState := &SessionState{}
    //fill it using .GetSession()
    token, err := manager.GetState(r, sessionState)
    if err != nil {
        //...handle error...
    }

    //...use sessionState...
}
Ending Sessions

To end a session, simply call manager.EndSession() passing the current request. This will get and verify the token from the request, and then delete the session state from the Store. Once the token and associated session state is deleted, the token will be treated as invalid on all subsequent requests.

func SignOutHandler(w http.ResponseWriter, r *http.Request) {
    //end the current session
    if err := manager.EndSession(r); err != nil {
        //...handle error...
    }
}

Modular Usage

The Manager object uses the Authorization HTTP header to transmit the session token. If you would prefer to use a different header, or perhaps a cookie, you can use the Token and Store objects directly. For example:

func SignInHandler(w http.ResponseWriter, r *http.Request) {
    //...authentication code...

    //construct your own session state struct
    sessionState := NewSessionState(/* ... */)

    //generate a new session Token, passing your signingKey
    token, err := NewToken(signignKey)
    if err != nil {
        //...handle error...
        //this would only happen if the system didn't have
        //enough entropy to generate enough random bytes
    }

    //save the state to the store
    if err := sessionStore.Save(token, sessionState); err != nil {
        //...handle error...
    }

    //...include token in response...
    //you can use a cookie, or a different header, or put it in the response body
}

Documentation

Overview

Package sessions provides a simple, modular sessions package for Go web services.

Installation

Use `go get` to install:

go get github.com/davestearns/sessions

Basic Usage

Start by constructing a session `Store` in your startup code. For example, to create a new redis store, use code like this:

import (
	"time"
	"github.com/davestearns/sessions"
)

func main() {
	redisAddr := // ... network address of your redis server
	store := sessions.NewRedisStore(
		sessions.NewRedisPool(redisAddr, time.Minute*5),
		time.Hour)
}

The `NewRedisPool()` function creates a new `redis.Pool` instance that is configured with defaults that should work well in most situations. The time duration passed as the second parameter controls when the pool will do a health check on the connection: if the connection has been idle for longer than the duration, the pool will execute a `PING` request to ensure that the connection is still alive.

The time duration passed as the second parameter to `NewRedisStore()` controls the time-to-live for session state. The TTL is reset each time you get the state, so this controls how long idle sessions will remain before expiring.

Next, construct a `Manager` and give it your token signing key(s), along with your store. The keys are used to digitally sign the session tokens returned to clients, so that we can easily detect attempts to modify the token to session-hop. If you supply more than one key, the manager will rotate which key it uses, making it harder for an attacker to crack your signing key.

//load your token signing keys from environment variables or wherever
signingKeys := []string{os.GetEnv(SIGNKEY_1), os.GetEnv(SIGNKEY_2)}
manager := sessions.NewManager(sessions.DefaultIDLength, signingKeys, store)

To begin a session within one of your handler functions, use `manager.BeginSession()`:

func SignInHandler(w http.ResponseWriter, r *http.Request) {
	//...authentication code...

	//construct and initialize your own session state struct
	sessionState := NewSessionState(...)

	//begin a new session: this will add an Authorization header to the response
	//containing the new session token. The new token is returned in case you
	//want to do something with it.
	token, err := manager.BeginSession(w, sessionState)
	if err != nil {
		//...handle error...
	}

	//...write response body...
}

The `.BeginSession()` method will add an `Authorization` header to the response containing the value `Bearer <token-string>`. The `<token-string>` will be a base64-encoded version of the newly-generated session token. The session ID portion of the token is a series of crypto-random bytes, the length of which is controlled by the `idLength` parameter passed to `sessions.NewManager`. The token also contains an HMAC signature of the ID, which is generated using one of your signing keys.

Clients should hold on to this `Authorization` response header value and send it back to the server with all subsequent requests. The `.GetState()` method described below will extract the session token from the `Authorization` request header, verify it, and fetch the associated state from the store. If the client attempted to modify the token, the HMAC signature verification will fail, an the token will be considered invalid.

This package uses the `Authorization` header instead of a cookie to avoid CSRF attacks (see https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)). Since `Authorization` headers are not handled automatically by the browser, they are not susceptible to typical CSRF attacks, but they do require some client-side JavaScript to receive the response header value, and include that value in the `Authorization` header on all subsequent requests.

If you would prefer to use a cookie, or some other method of transmission, you can bypass the `Manager` object and use the `Token` and `Store` objects directly.

For strategies on how you can share your global `Manager` instance with your handler functions see https://drstearns.github.io/tutorials/gohandlerctx/.

To get the previously-saved session state during subsequent requests, use `manager.GetState()`:

func SomeStatefulHandler(w http.ResponseWriter, r *http.Request) {
	//create a new empty session state
	sessionState := &SessionState{}
	//fill it using .GetSession()
	token, err := manager.GetState(r, sessionState)
	if err != nil {
		//...handle error...
	}

	//...use sessionState...
}

To end a session, simply call `manager.EndSession()` passing the current request. This will get and verify the token from the request, and then delete the session state from the `Store`. Once the token and associated session state is deleted, the token will be treated as invalid on all subsequent requests.

func SignOutHandler(w http.ResponseWriter, r *http.Request) {
	//end the current session
	if err := manager.EndSession(r); err != nil {
		//...handle error...
	}
}

Index

Constants

View Source
const DefaultIDLength = 32

DefaultIDLength is the default ID byte length.

View Source
const MinIDLength = 16

MinIDLength is the minimum ID byte length allowed. See https://www.owasp.org/index.php/Insufficient_Session-ID_Length

Variables

View Source
var ErrNoToken = errors.New("no session token")

ErrNoToken is returned from GetState and EndSession when there is no session token in the provided request

View Source
var ErrUnsupportedTokenType = errors.New("unsupported session token type")

ErrUnsupportedTokenType is returned when the type prefix for the session token is not supported

Functions

func NewRedisPool

func NewRedisPool(addr string, testAfterIdle time.Duration) *redis.Pool

NewRedisPool constructs a redis.Pool with defaults that should work well in most situations. If the pool returns a connection that has beeen idle for testAfterIdle or longer, it will be health-tested by executing a PING. Set testAfterIdle to 0 to always health-test existing connections before they are returned. Callers may adjust any settings on the returned pool before passing it to NewRedisStore().

Types

type ID

type ID interface {
	//Len returns the length of the session ID in bytes
	Len() int
	//String returns a base64-encoded version of the ID,
	//suitable for use as a key in a session store
	String() string
}

ID provides read-only access to the ID portion of the token.

type Manager

type Manager interface {
	BeginSession(w http.ResponseWriter, sessionState interface{}) (Token, error)
	GetToken(r *http.Request) (Token, error)
	GetState(r *http.Request, sessionState interface{}) (Token, error)
	UpdateState(token Token, sessionState interface{}) error
	EndSession(r *http.Request) error
}

Manager describes what session managers can do

func NewManager

func NewManager(idLength int, signingKeys []string, store Store) Manager

NewManager constructs a new manager. Use idLength to specify a byte length for newly-generate session IDs (see DefaultIDLength). Pass one or more signingKeys to use for signing session tokens--if multiple are provided, the manager will rotate which key is used over time. The store will be used to save, get, and delete session state associated with tokens.

type RedisStore

type RedisStore struct {
	//Used for key expiry time on redis. Callers
	//may adjust this after construction.
	SessionDuration time.Duration
	// contains filtered or unexported fields
}

RedisStore represents a Store backed by redis.

func NewRedisStore

func NewRedisStore(pool *redis.Pool, sessionDuration time.Duration) *RedisStore

NewRedisStore constructs a new RedisStore

func (*RedisStore) Delete

func (rs *RedisStore) Delete(token Token) error

Delete deletes all session state data associated with the provided session token.

func (*RedisStore) Get

func (rs *RedisStore) Get(token Token, sessionState interface{}) error

Get gets the session state associated with the provided session token, and resets the expiry time. The previously-stored state will be decoded into the sessionState value, so that must be passed by reference.

func (*RedisStore) Save

func (rs *RedisStore) Save(token Token, sessionState interface{}) error

Save saves the provided sessionState into the store, associated with the provided session token. The sessionState must be gob-encodable.

type Store

type Store interface {
	//Save saves the sessionState to the store, associated with the token
	Save(token Token, sessionState interface{}) error
	//Get populates sessionState with the data previously saved with the token
	Get(token Token, sessionState interface{}) error
	//Delete removes state associated with the token
	Delete(token Token) error
}

Store describes what a session store can do

type Token

type Token interface {
	//String returns a base64-encoded version of the entire token
	String() string
	//ID returns a read-only interface to the ID portion of the token
	ID() ID
}

Token represents a crypto-randon, digitally-signed session token. Use NewToken() or NewTokenFromReader() to generate a new token. Use the String() method to generate a base64-encoded version of the token that is safe to transport over HTTPS, and use VerifyToken to verify a base64-encoded token sent by the client.

func NewToken

func NewToken(signingKey []byte) (Token, error)

NewToken constructs a new Token of DefaultIDLength, using the provided signingKey for generating the HMAC signature.

func NewTokenOfLength

func NewTokenOfLength(signingKey []byte, idLength int) (Token, error)

NewTokenOfLength constructs a new Token using idLength as the length of the session ID in bytes (must be >= MinIDLength). The signingKey must be non-zero length, and will be used with the HMAC algorithm to digitally sign the ID.

func VerifyToken

func VerifyToken(b64token string, signingKey []byte) (Token, error)

VerifyToken verifies a base64-encoded token string using the provided signingKey.

Jump to

Keyboard shortcuts

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