sessions

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: May 2, 2017 License: MIT Imports: 7 Imported by: 4

README

Build Status Coverage Status Go Report Card GoDoc

Sessions

A dead simple, highly customizable sessions service for go http servers

README Contents:

  1. Quickstart
  2. Performance
  3. API
  4. Test Coverage
  5. Example
  6. License

Quickstart

package main

import (
    ...
)

var sesh *sessions.Service

var issueSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userSession, seshErr := sesh.IssueUserSession("fakeUserID", "", w)
	if seshErr != nil {
		log.Printf("Err issuing user session: %v\n", seshErr)
		http.Error(w, seshErr.Err.Error(), seshErr.Code) // seshErr is a custom err with an http code
		return
	}
	log.Printf("In issue; user's session: %v\n", userSession)

	w.WriteHeader(http.StatusOK)
})

func main() {
	seshStore := store.New(store.Options{})

	// e.g. `$ openssl rand -base64 64`
	authKey := "DOZDgBdMhGLImnk0BGYgOUI+h1n7U+OdxcZPctMbeFCsuAom2aFU4JPV4Qj11hbcb5yaM4WDuNP/3B7b+BnFhw=="
	authOptions := auth.Options{
		Key: []byte(authKey),
	}
	seshAuth, err := auth.New(authOptions)
	if err != nil {
		log.Fatal(err)
	}

	transportOptions := transport.Options{
		Secure: false, // note: can't use secure cookies in development!
	}
	seshTransport := transport.New(transportOptions)

	seshOptions := sessions.Options{}
	sesh = sessions.New(seshStore, seshAuth, seshTransport, seshOptions)

	http.HandleFunc("/issue", issueSession)

    log.Println("Listening on localhost:8080")
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

Performance

Benchmarks require a redis-server running. Set the REDIS_URL environment variable, otherwise the benchmarks look for ":6379".

YMMV

$ (cd benchmark && go test -bench=.)

setting up benchmark tests
BenchmarkBaseServer-2              20000             72479 ns/op
BenchmarkValidSession-2            10000            151650 ns/op
PASS
shutting down benchmark tests
ok      github.com/adam-hanna/sessions/benchmark        3.727s

API

user.Session
type Session struct {
	ID        string
	UserID    string
	ExpiresAt time.Time
	JSON      string
}

Session is the struct that is used to store session data. The JSON field allows you to set any custom information you'd like. See the example

IssueUserSession
func (s *Service) IssueUserSession(userID string, json string, w http.ResponseWriter) (*user.Session, *sessionerrs.Custom)

IssueUserSession grants a new user session, writes that session info to the store and writes the session on the http.ResponseWriter.

This method should be called when a user logs in, for example.

ClearUserSession
func (s *Service) ClearUserSession(userSession *user.Session, w http.ResponseWriter) *sessionerrs.Custom

ClearUserSession is used to remove the user session from the store and clear the cookies on the ResponseWriter.

This method should be called when a user logs out, for example.

GetUserSession
func (s *Service) GetUserSession(r *http.Request) (*user.Session, *sessionerrs.Custom)

GetUserSession returns a user session from a request. This method only returns valid sessions. Therefore, sessions that have expired, or that fail signature verification will return a custom session error with code 401.

ExtendUserSession
func (s *Service) ExtendUserSession(userSession *user.Session, r *http.Request, w http.ResponseWriter) *sessionerrs.Custom

ExtendUserSession extends the ExpiresAt of a session by the Options.ExpirationDuration

Note that this function must be called, manually! Extension of user session expiry's does not happen automatically!

Testing Coverage

ok      github.com/adam-hanna/sessions				9.012s  coverage: 94.1% of statements
?       github.com/adam-hanna/sessions/sessionerrs	[no test files]
ok      github.com/adam-hanna/sessions/auth			0.003s  coverage: 100.0% of statements
ok      github.com/adam-hanna/sessions/store		0.006s  coverage: 85.4% of statements
ok      github.com/adam-hanna/sessions/benchmark	0.004s  coverage: 0.0% of statements [no tests to run]
ok      github.com/adam-hanna/sessions/transport	0.004s  coverage: 95.2% of statements
ok      github.com/adam-hanna/sessions/user			0.003s  coverage: 100.0% of statements

Tests are broken down into three categories: unit, integration and e2e. Integration and e2e tests require a connection to a redis server. The connection address can be set in the REDIS_URL environment variable. The default is ":6379".

To run all tests, simply:

$ go test -tags="unit integration e2e" ./...

// or
$ make test

// or
$ make test-cover-html && go tool cover -html=coverage-all.out

To run only tests from one of the categories:

$ go test -tags="integration" ./...

To run only unit and integration tests:

$ go test -tags="unit integration" ./...

Example

The following example is a demonstration of using the session service along with a CSRF code to check for authentication. The CSRF code is stored in the userSession JSON field.

package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"time"

	"github.com/adam-hanna/sessions"
	"github.com/adam-hanna/sessions/auth"
	"github.com/adam-hanna/sessions/store"
	"github.com/adam-hanna/sessions/transport"
)

// SessionJSON is used for marshalling and unmarshalling custom session json information.
// We're using it as an opportunity to tie csrf strings to sessions to prevent csrf attacks
type SessionJSON struct {
	CSRF string `json:"csrf"`
}

var sesh *sessions.Service

var issueSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	csrf, err := generateKey()
	if err != nil {
		log.Printf("Err generating csrf: %v\n", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	myJSON := SessionJSON{
		CSRF: csrf,
	}
	JSONBytes, err := json.Marshal(myJSON)
	if err != nil {
		log.Printf("Err generating json: %v\n", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	userSession, seshErr := sesh.IssueUserSession("fakeUserID", string(JSONBytes[:]), w)
	if seshErr != nil {
		log.Printf("Err issuing user session: %v\n", seshErr)
		http.Error(w, seshErr.Err.Error(), seshErr.Code)
		return
	}
	log.Printf("In issue; user's session: %v\n", userSession)

	// note: we set the csrf in a cookie, but look for it in request headers
	csrfCookie := http.Cookie{
		Name:     "csrf",
		Value:    csrf,
		Expires:  userSession.ExpiresAt,
		Path:     "/",
		HttpOnly: false,
		Secure:   false, // note: can't use secure cookies in development
	}
	http.SetCookie(w, &csrfCookie)

	w.WriteHeader(http.StatusOK)
})

var requiresSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userSession, seshErr := sesh.GetUserSession(r)
	if seshErr != nil {
		log.Printf("Err fetching user session: %v\n", seshErr)
		http.Error(w, seshErr.Err.Error(), seshErr.Code)
		return
	}
	log.Printf("In require; user session expiration before extension: %v\n", userSession.ExpiresAt.UTC())

	myJSON := SessionJSON{}
	if err := json.Unmarshal([]byte(userSession.JSON), &myJSON); err != nil {
		log.Printf("Err issuing unmarshalling json: %v\n", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	log.Printf("In require; user's custom json: %v\n", myJSON)

	// note: we set the csrf in a cookie, but look for it in request headers
	csrf := r.Header.Get("X-CSRF-Token")
	if csrf != myJSON.CSRF {
		log.Printf("Unauthorized! CSRF token doesn't match user session")
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// note that session expiry's need to be manually extended
	seshErr = sesh.ExtendUserSession(userSession, r, w)
	if seshErr != nil {
		log.Printf("Err fetching user session: %v\n", seshErr)
		http.Error(w, seshErr.Err.Error(), seshErr.Code)
		return
	}
	log.Printf("In require; users session expiration after extension: %v\n", userSession.ExpiresAt.UTC())

	// need to extend the csrf cookie, too
	csrfCookie := http.Cookie{
		Name:     "csrf",
		Value:    csrf,
		Expires:  userSession.ExpiresAt,
		Path:     "/",
		HttpOnly: false,
		Secure:   false, // note: can't use secure cookies in development
	}
	http.SetCookie(w, &csrfCookie)

	w.WriteHeader(http.StatusOK)
})

var clearSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userSession, err := sesh.GetUserSession(r)
	if err != nil {
		log.Printf("Err fetching user session: %v\n", err)
		http.Error(w, err.Err.Error(), err.Code)
		return
	}

	log.Printf("In clear; session: %v\n", userSession)

	myJSON := SessionJSON{}
	if err := json.Unmarshal([]byte(userSession.JSON), &myJSON); err != nil {
		log.Printf("Err issuing unmarshalling json: %v\n", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	log.Printf("In require; user's custom json: %v\n", myJSON)

	// note: we set the csrf in a cookie, but look for it in request headers
	csrf := r.Header.Get("X-CSRF-Token")
	if csrf != myJSON.CSRF {
		log.Printf("Unauthorized! CSRF token doesn't match user session")
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	err = sesh.ClearUserSession(userSession, w)
	if err != nil {
		log.Printf("Err clearing user session: %v\n", err)
		http.Error(w, err.Err.Error(), err.Code)
		return
	}

	// need to clear the csrf cookie, too
	aLongTimeAgo := time.Now().Add(-1000 * time.Hour)
	csrfCookie := http.Cookie{
		Name:     "csrf",
		Value:    "",
		Expires:  aLongTimeAgo,
		Path:     "/",
		HttpOnly: false,
		Secure:   false, // note: can't use secure cookies in development
	}
	http.SetCookie(w, &csrfCookie)

	w.WriteHeader(http.StatusOK)
})

func main() {
	seshStore := store.New(store.Options{})

	// e.g. `$ openssl rand -base64 64`
	authKey := "DOZDgBdMhGLImnk0BGYgOUI+h1n7U+OdxcZPctMbeFCsuAom2aFU4JPV4Qj11hbcb5yaM4WDuNP/3B7b+BnFhw=="
	authOptions := auth.Options{
		Key: []byte(authKey),
	}
	seshAuth, err := auth.New(authOptions)
	if err != nil {
		log.Fatal(err)
	}

	transportOptions := transport.Options{
		Secure: false, // note: can't use secure cookies in development!
	}
	seshTransport := transport.New(transportOptions)

	seshOptions := sessions.Options{}
	sesh = sessions.New(seshStore, seshAuth, seshTransport, seshOptions)

	http.HandleFunc("/issue", issueSession)
	http.HandleFunc("/require", requiresSession)
	http.HandleFunc("/clear", clearSession) // also requires a valid session

	log.Println("Listening on localhost:3000")
	log.Fatal(http.ListenAndServe("127.0.0.1:3000", nil))
}

func generateKey() (string, error) {
	b := make([]byte, 16)
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		return "", err
	}
	return base64.URLEncoding.EncodeToString(b), nil
}

License

The MIT License (MIT)

Copyright (c) 2017 Adam Hanna

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Documentation

Index

Constants

View Source
const (
	// DefaultExpirationDuration sets the default session expiration duration
	DefaultExpirationDuration = 3 * 24 * time.Hour // 3 days
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Options

type Options struct {
	ExpirationDuration time.Duration
}

Options defines the behavior of the session service

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service provides session service for http servers

func New

New returns a new session service

func (*Service) ClearUserSession

func (s *Service) ClearUserSession(userSession *user.Session, w http.ResponseWriter) *sessionerrs.Custom

ClearUserSession is used to remove the user session from the store and clear the cookies on the ResponseWriter.

This method should be called when a user logs out, for example.

func (*Service) ExtendUserSession

func (s *Service) ExtendUserSession(userSession *user.Session, r *http.Request, w http.ResponseWriter) *sessionerrs.Custom

ExtendUserSession extends the ExpiresAt of a session by the Options.ExpirationDuration

Note that this function must be called, manually! Extension of user session expiry's does not happen automatically!

func (*Service) GetUserSession

func (s *Service) GetUserSession(r *http.Request) (*user.Session, *sessionerrs.Custom)

GetUserSession returns a user session from a request. This method only returns valid sessions. Therefore, \ sessions that have expired, or that fail signature verification will return a custom session error with code 401

func (*Service) IssueUserSession

func (s *Service) IssueUserSession(userID string, json string, w http.ResponseWriter) (*user.Session, *sessionerrs.Custom)

IssueUserSession grants a new user session, writes that session info to the store \ and writes the session on the http.ResponseWriter.

This method should be called when a user logs in, for example.

type ServiceInterface

type ServiceInterface interface {
	IssueUserSession(userID string, json string, w http.ResponseWriter) (*user.Session, *sessionerrs.Custom)
	ClearUserSession(userSession *user.Session, w http.ResponseWriter) *sessionerrs.Custom
	GetUserSession(r *http.Request) (*user.Session, *sessionerrs.Custom)
	ExtendUserSession(userSession *user.Session, r *http.Request, w http.ResponseWriter) *sessionerrs.Custom
}

ServiceInterface defines the methods performed by the session service

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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