Skip to content

Commit

Permalink
finished writing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-hanna committed May 1, 2017
1 parent bb4308d commit 2a2692d
Show file tree
Hide file tree
Showing 19 changed files with 1,915 additions and 15 deletions.
274 changes: 274 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# Go Sessions
A dead simple, highly customizable sessions service for go http servers

## Quickstart

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

## Testing
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" ./...
~~~

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.

~~~go
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
}
~~~
22 changes: 16 additions & 6 deletions auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import (
"github.com/adam-hanna/sessions/sessionerrs"
)

// note @adam-hanna: can these be constants?
var (
// ErrNoSessionKey is thrown when no key was provided for HMAC signing
ErrNoSessionKey = errors.New("no session key")
// ErrMalformedSession is thrown when the session value doesn't conform to expectations
ErrMalformedSession = errors.New("malformed session")
// ErrInvalidSessionSignature the signature included with the session can't be verified with the provided session key
ErrInvalidSessionSignature = errors.New("invalid session signature")
)

// Service performs signing and verification actions using HMAC
type Service struct {
options Options
Expand All @@ -21,17 +31,17 @@ type Options struct {
func New(options Options) (*Service, *sessionerrs.Custom) {
// note @adam-hanna: should we perform other checks like min/max length?
if len(options.Key) == 0 {
return &Service{}, &sessionerrs.Custom{
return nil, &sessionerrs.Custom{
Code: 500,
Err: errors.New("no session key"),
Err: ErrNoSessionKey,
}
}
return &Service{
options: options,
}, nil
}

// SignAndBase64Encode signs the sessionID with they key and returns a base64 encoded string
// SignAndBase64Encode signs the sessionID with the key and returns a base64 encoded string
func (s *Service) SignAndBase64Encode(sessionID string) (string, *sessionerrs.Custom) {
userSessionIDBytes := []byte(sessionID)
signedBytes := signHMAC(&userSessionIDBytes, &s.options.Key)
Expand All @@ -55,12 +65,12 @@ func (s *Service) VerifyAndDecode(signed string) (string, *sessionerrs.Custom) {
}
}

// note: session uuid's are always 36 bytes long
// note: session uuid's are always 36 bytes long. This will make it difficult to switch to a new uuid algorithm!
if len(decodedSessionValueBytes) <= 36 {
// note @adam-hanna: is 401 the proper http status code, here?
return "", &sessionerrs.Custom{
Code: 401,
Err: errors.New("invalid session"),
Err: ErrMalformedSession,
}
}
sessionIDBytes := decodedSessionValueBytes[:36]
Expand All @@ -72,7 +82,7 @@ func (s *Service) VerifyAndDecode(signed string) (string, *sessionerrs.Custom) {
if !verified {
return "", &sessionerrs.Custom{
Code: 401,
Err: errors.New("invalid session signature"),
Err: ErrInvalidSessionSignature,
}
}

Expand Down
Loading

0 comments on commit 2a2692d

Please sign in to comment.