A dead simple, highly performant, highly customizable sessions middleware for go http servers.

Build Status Coverage Status Go Report Card GoDoc

If you're interested in jwt's, see my jwt library!

Sessions

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

By default, the service stores sessions in redis, and transports sessions to clients in cookies. However, these are easily customizeable. For instance, the storage interface only implements three methods:

// ServiceInterface defines the behavior of the session store
type ServiceInterface interface {
	SaveUserSession(userSession *user.Session) error
	DeleteUserSession(sessionID string) error
	FetchValidUserSession(sessionID string) (*user.Session, error)
}

README Contents:

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

Quickstart

var sesh *sessions.Service

// issue a new session and write the session to the ResponseWriter
userSession, err := sesh.IssueUserSession("fakeUserID", "{\"foo\":\"bar\"}", w)
if err != nil {
	log.Printf("Err issuing user session: %v\n", err)
	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	return
}

...

// Fetch a pointer to a valid user session from a request. A nil pointer indicates no or invalid session
userSession, err := sesh.GetUserSession(r)
if err != nil {
	log.Printf("Err fetching user session: %v\n", err)
	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	return
}
// nil session pointers indicate a 401 unauthorized
if userSession == nil {
	http.Error(w, "Unathorized", http.StatusUnauthorized)
	return
}

...

// Extend session expiry. Note that session expiry's need to be manually extended
if err := sesh.ExtendUserSession(userSession, r, w); err != nil {
	log.Printf("Err extending user session: %v\n", err)
	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	return
}

...

// Invalidate a user session, deleting it from redis and expiring the cookie on the ResponseWriter
if err := sesh.ClearUserSession(userSession, w); err != nil {
	log.Printf("Err clearing user session: %v\n", err)
	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	return
}

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

Design

By default, the service stores sessions in redis, and transports hashed sessionIDs to clients in cookies. However, these are easily customizeable through the creation of custom structs that implement the interface.

The general flow of the session service is as follows:

  1. Create store, auth and transport services by calling their respective New(...) functions (or create your own custom services that implement the service's interface methods). Then pass these services to the sessions.New(...) constructor.
  2. After a user logs in, call the sessions.IssueUserSession(...) function. This function first creates a new user.Session. SessionIDs are RFC 4122 version 4 uuids. Next, the service hashes the sessionID with the provided key. The hashing algorithm is SHA-512, and therefore the key used should be between 64 and 128 bytes. Then, the service stores the session in redis and finally writes the hashed sessionID to the response writer in a cookie. Sessions written to the redis db utilize EXPIREAT to automatically destory expired sessions.
  3. To check if a valid session was included in a request, use the sessions.GetUserSession(...) function. This function grabs the hashed sessionID from the session cookie, verifies the HMAC signature and finally looks up the session in the redis db. If the session is expired, or fails HMAC signature verification, this function will return a nil pointer to a user session. If the session is valid, and you'd like to extend the session's expiry, you can then call session.ExtendUserSession(...). Session expiry's are never automatically extended, only through calling this function will the session's expiry be extended.
  4. When a user logs out, call the sessions.ClearUserSession(...) function. This function destroys the session in the db and also destroys the cookie on the ResponseWriter.

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, error)

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) error

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, error)

GetUserSession returns a user session from the hashed sessionID included in the request. This method only returns valid sessions. Therefore, sessions that have expired or that fail signature verification will return a nil pointer.

ExtendUserSession

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

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
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 user.Session 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, "Internal Server Error", http.StatusInternalServerError)
		return
	}

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

	userSession, err := sesh.IssueUserSession("fakeUserID", string(JSONBytes[:]), w)
	if err != nil {
		log.Printf("Err issuing user session: %v\n", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		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, err := sesh.GetUserSession(r)
	if err != nil {
		log.Printf("Err fetching user session: %v\n", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	// nil session pointers indicate a 401 unauthorized
	if userSession == nil {
		http.Error(w, "Unathorized", http.StatusUnauthorized)
		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 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
	if err = sesh.ExtendUserSession(userSession, r, w); err != nil {
		log.Printf("Err extending user session: %v\n", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		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, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	// nil session pointers indicate a 401 unauthorized
	if userSession == nil {
		http.Error(w, "Unathorized", http.StatusUnauthorized)
		return
	}

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

	myJSON := SessionJSON{}
	if err := json.Unmarshal([]byte(userSession.JSON), &myJSON); err != nil {
		log.Printf("Err unmarshalling json: %v\n", err)
		http.Error(w, "Internal Server 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
	}

	if err = sesh.ClearUserSession(userSession, w); err != nil {
		log.Printf("Err clearing user session: %v\n", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		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`
	seshAuth, err := auth.New(auth.Options{
		Key: []byte("DOZDgBdMhGLImnk0BGYgOUI+h1n7U+OdxcZPctMbeFCsuAom2aFU4JPV4Qj11hbcb5yaM4WDuNP/3B7b+BnFhw=="),
	})
	if err != nil {
		log.Fatal(err)
	}

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

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

	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))
}

// thanks
// https://astaxie.gitbooks.io/build-web-application-with-golang/en/06.2.html#unique-session-ids
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.
Owner
Adam Hanna
An entrepreneur living and working in Los Angeles. My RSA fingerprint: 3D33 EE0E 8620 44EC F7FA 3CA0 2687 062E 2A4E 0E15
Adam Hanna
Similar Resources

ACL, RBAC, ABAC authorization middleware for KubeSphere

casbin-kubesphere-auth Casbin-kubesphere-auth is a plugin which apply several security authentication check on kubesphere via casbin. This plugin supp

Jun 9, 2022

OAuth 2.0 middleware service for chi (ported from gin by community member)

oauth middleware OAuth 2.0 Authorization Server & Authorization Middleware for go-chi This library was ported to go-chi from https://github.com/maxzer

Dec 8, 2022

fiber api key authentication middleware

fiber-key-auth Secure your fiber endpoints using API keys. Report Bug · Request Feature Table of Contents About The Project Built With Getting Started

Dec 14, 2022

Auth Middleware for session & white-listed routing

Auth Middleware for session & white-listed routing

Nov 4, 2021

JWT and Permission Middleware with MongoRPC

JWT and Permission Middleware with MongoRPC

Nov 19, 2021

Basic and Digest HTTP Authentication for golang http

HTTP Authentication implementation in Go This is an implementation of HTTP Basic and HTTP Digest authentication in Go language. It is designed as a si

Dec 22, 2022

HTTP-server-with-auth# HTTP Server With Authentication

HTTP-server-with-auth# HTTP Server With Authentication Introduction You are to use gin framework package and concurrency in golang and jwt-go to imple

Nov 9, 2022

A very simple HTTP reverse proxy that checks that requests contain a valid secret as a bearer token

bearproxy -- Authorization enforcing HTTP reverse proxy Bearproxy is a very simple HTTP reverse proxy that checks that requests contain a valid secret

Nov 11, 2021

Simple Go/Chi powered http server meant for ad hoc use such as exposing a file system for testing HTML.

httphere httphere is a simple Go/Chi powered http server for ad hoc use such as testing HTML or temporarily exposing a local file system at the curren

Dec 10, 2021
Comments
  • Missing .gitmodules breaks golang/dep

    Missing .gitmodules breaks golang/dep

    What version of dep are you using (dep version)?

    go: go1.9.1 linux/amd64 dep: v0.3.2-4-g7b5c43aa git: 2.14.2

    What dep command did you run?

    # dep ensure -v
    Root project is "github.com/my/repo"
     8 transitively valid internal packages
     23 external packages imported from 16 projects
    (0)   ✓ select (root)
    ...
    (4)     ? attempt github.com/NYTimes/logrotate with 1 pkgs; at least 1 versions to try
    (4)         try github.com/NYTimes/logrotate@master
    (4)     ✓ select github.com/NYTimes/logrotate@master w/1 pkgs
    (5)     ? attempt github.com/aymerick/raymond with 1 pkgs; at least 1 versions to try
    (5)         try github.com/aymerick/[email protected]
    (5)     ✓ select github.com/aymerick/[email protected] w/4 pkgs
    (6)     ? attempt github.com/adam-hanna/sessions with 5 pkgs; at least 1 versions to try
    (6)         try github.com/adam-hanna/[email protected]
    (6)     ✗   unexpected error while defensively updating submodules: : command failed: [git submodule update --init --recursive]: exit status 128
    (6)         try github.com/adam-hanna/[email protected]
    (7)     ✗   github.com/adam-hanna/[email protected] not allowed by constraint ^0.0.2:
    (7)         ^0.0.2 from (root)
    (6)         try github.com/adam-hanna/sessions@master
    (7)     ✗   github.com/adam-hanna/sessions@master not allowed by constraint ^0.0.2:
    (7)         ^0.0.2 from (root)
    (6)         try github.com/adam-hanna/sessions@develop
    (7)     ✗   github.com/adam-hanna/sessions@develop not allowed by constraint ^0.0.2:
    (7)         ^0.0.2 from (root)
    (6)       ← no more versions of github.com/adam-hanna/sessions to try; begin backtrack
    (5)     ← backtrack: no more versions of github.com/aymerick/raymond to try
    (4)     ← backtrack: no more versions of github.com/NYTimes/logrotate to try
    ...
      ✗ solving failed
    
    Solver wall times by segment:
         b-source-exists: 3.913478065s
             b-list-pkgs: 1.286524935s
                  b-gmal: 218.790765ms
                 satisfy:   1.505631ms
                new-atom:   1.238137ms
               backtrack:   1.062701ms
             select-root:    909.949µs
             select-atom:     643.78µs
                unselect:    335.459µs
      b-deduce-proj-root:     89.125µs
              b-pair-rev:     85.236µs
         b-list-versions:     23.727µs
               b-matches:     15.354µs
                   other:      9.864µs
    
      TOTAL: 5.424712728s
    
    ensure Solve(): No versions of github.com/adam-hanna/sessions met constraints:
            v0.0.2: unexpected error while defensively updating submodules: : command failed: [git submodule update --init --recursive]: exit status 128
            v0.0.1: Could not introduce github.com/adam-hanna/[email protected], as it is not allowed by constraint ^0.0.2 from project github.com/my/repo.
            master: Could not introduce github.com/adam-hanna/sessions@master, as it is not allowed by constraint ^0.0.2 from project github.com/my/repo.
            develop: Could not introduce github.com/adam-hanna/sessions@develop, as it is not allowed by constraint ^0.0.2 from project github.com/my/repo.
    

    What did you expect to see?

    Working vendor checkout.

    What did you see instead?

    The reason is missing .gitmodules. After cloning your repo by hand and executing git submodule update --init --recursive you get:

    # git submodule update --init --recursive
    fatal: No url found for submodule path 'vendor/github.com/pborman/uuid' in .gitmodules
    # echo $?
    128
    

    Is there any reason for skipping .gitmodules?

  • Remove sessionerrs.Custom in favor of standard golang error

    Remove sessionerrs.Custom in favor of standard golang error

    Code should not return a custom err. Rather, to distinguish between a 401 and 500, the session service functions should return a nil pointer to a user session if none is present or valid, thus triggering a http 401 status code.

  • New store should ping store

    New store should ping store

    https://github.com/adam-hanna/sessions/blob/cf0bbdb03181312e5baf128170c26bed046aaf77/store/service.go#L44

    The New function should return an error and ping the store.

  • Handle setDefaultOptions for all options

    Handle setDefaultOptions for all options

    The problem is that it's difficult to know if an option type of int or bool was never initialized, or was initialized to its default value.

    Possibly use strings?

This package provides json web token (jwt) middleware for goLang http servers

jwt-auth jwt auth middleware in goLang. If you're interested in using sessions, checkout my sessions library! README Contents: Quickstart Performance

Dec 5, 2022
🍍Jeff provides the simplest way to manage web sessions in Go.

jeff A tool for managing login sessions in Go. Motivation I was looking for a simple session management wrapper for Go and from what I could tell ther

Jan 4, 2023
makes it easy to keep track of user sessions on a Go API.

usersession is a simple way to keep track of user information on a Go API. it assigns a session ID and gives you a place to store the IP and some user

Dec 22, 2021
Auth Go microservice for managing authentication sessions

cryptomath-go-auth Auth Go microservice for managing authentication sessions. Install dependencies $ make deps Build $ make vendor $ make build Databa

Mar 4, 2022
A simple passwordless proxy authentication middleware using email.
A simple passwordless proxy authentication middleware using email.

email proxy auth A simple passwordless proxy authentication middleware that uses only email as the authentication provider. Motivation I wanted to res

Jul 27, 2022
A simple passwordless authentication middleware that uses only email as the authentication provider
A simple passwordless authentication middleware that uses only email as the authentication provider

email auth A simple passwordless authentication middleware that uses only email as the authentication provider. Motivation I wanted to restrict access

Jul 27, 2022
Go session management for web servers (including support for Google App Engine - GAE).

Session The Go standard library includes a nice http server, but unfortunately it lacks a very basic and important feature: HTTP session management. T

Oct 10, 2022
SSH Manager - manage authorized_keys file on remote servers

SSH Manager - manage authorized_key file on remote servers This is a simple tool that I came up after having to on-boarding and off-boarding developer

Dec 6, 2022
:closed_lock_with_key: Middleware for keeping track of users, login states and permissions

Permissions2 Middleware for keeping track of users, login states and permissions. Online API Documentation godoc.org Features and limitations Uses sec

Dec 31, 2022
fastglue-csrf implements CSRF middleware for fastglue.

fastglue-csrf Overview fastglue-csrf implements CSRF middleware for fastglue.

Jan 5, 2022