Skip to content

Commit

Permalink
finished abstracting code base
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-hanna committed Apr 28, 2017
1 parent db63622 commit 0953482
Show file tree
Hide file tree
Showing 16 changed files with 489 additions and 411 deletions.
73 changes: 73 additions & 0 deletions auth/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package auth

import (
"errors"

"github.com/adam-hanna/sessions/sessionerrs"
)

// Service performs signing and verification actions using HMAC
type Service struct {
options Options
}

// Options defines the behavior of the auth service
type Options struct {
// Key is a slice of bytes for performing HMAC signing and verification operations
Key []byte
}

// New returns a new auth service
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{
Code: 500,
Err: errors.New("no session key"),
}
}
return &Service{
options: options,
}, nil
}

// SignAndBase64Encode signs the sessionID with they 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)

// append the signature to the session id
sessionValBytes := make([]byte, len(userSessionIDBytes)+len(signedBytes))
sessionValBytes = append(userSessionIDBytes, signedBytes...)

return string(encode(sessionValBytes)[:]), nil
}

// VerifyAndDecode takes in a signed session string and returns a sessionID, only if the signed string passes
// auth verification.
func (s *Service) VerifyAndDecode(signed string) (string, *sessionerrs.Custom) {
sessionValueBytes := []byte(signed)
decodedSessionValueBytes, err := decode(sessionValueBytes)
if err != nil {
return "", &sessionerrs.Custom{
Code: 500,
Err: err,
}
}

// note: session uuid's are always 36 bytes long
sessionIDBytes := decodedSessionValueBytes[:36]
hmacBytes := decodedSessionValueBytes[36:]
// fmt.Printf("In auth.VerifyAndDecode\nsessionID: %s\nsig: %x\nkey: %s\n", string(sessionIDBytes[:]), string(hmacBytes[:]), string(s.options.Key[:]))

// verify the hmac signature
verified := verifyHMAC(&sessionIDBytes, &hmacBytes, &s.options.Key)
if !verified {
return "", &sessionerrs.Custom{
Code: 401,
Err: errors.New("invalid session signature"),
}
}

return string(sessionIDBytes[:]), nil
}
9 changes: 9 additions & 0 deletions auth/service_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package auth

import "github.com/adam-hanna/sessions/sessionerrs"

// ServiceInterface defines the methods that are performend by the auth service
type ServiceInterface interface {
SignAndBase64Encode(sessionID string) (string, *sessionerrs.Custom)
VerifyAndDecode(signed string) (string, *sessionerrs.Custom)
}
27 changes: 1 addition & 26 deletions sessions_util.go → auth/service_util.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sessions
package auth

import (
"crypto/hmac"
Expand All @@ -14,31 +14,6 @@ var (
ErrBase64Decode = errors.New("Base64 decoding failed")
)

// setDefaultOptions sets default values for nil fields
// note @adam-hanna: this utility function should be improved. The fields and types of the options struct \
// should not be hardcoded!
func setDefaultOptions(opts *Options) {
emptyOpts := Options{}
if opts.ExpirationDuration == emptyOpts.ExpirationDuration {
opts.ExpirationDuration = DefaultExpirationDuration
}
if opts.CSRFHeaderKey == emptyOpts.CSRFHeaderKey {
opts.CSRFHeaderKey = DefaultCSRFHeaderKey
}
}

// signAndEncodeSessionID signs the sessionID with they key and returns a base64 encoded byte slice
func (s *Service) signAndEncodeSessionID(sessionID string) []byte {
userSessionIDBytes := []byte(sessionID)
signedBytes := signHMAC(&userSessionIDBytes, &s.options.Key)

// append the signature to the session id
sessionValBytes := make([]byte, len(userSessionIDBytes)+len(signedBytes))
sessionValBytes = append(userSessionIDBytes, signedBytes...)

return encode(sessionValBytes)
}

// Thanks! https://github.com/gorilla/securecookie
// encode encodes a value using base64.
func encode(value []byte) []byte {
Expand Down
120 changes: 120 additions & 0 deletions service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package sessions

import (
"net/http"
"time"

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

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

// Service provides session service for http servers
type Service struct {
store store.ServiceInterface
auth auth.ServiceInterface
transport transport.ServiceInterface
options Options
}

// Options defines the behavior of the session service
type Options struct {
ExpirationDuration time.Duration
}

// New returns a new session service
func New(store store.ServiceInterface, auth auth.ServiceInterface, transport transport.ServiceInterface, options Options) *Service {
setDefaultOptions(&options)
return &Service{
store: store,
auth: auth,
transport: transport,
options: options,
}
}

// 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.
func (s *Service) IssueUserSession(userID string, json string, w http.ResponseWriter) (*user.Session, *sessionerrs.Custom) {
userSession := user.New(userID, json, s.options.ExpirationDuration)

// sign the session id
signedSessionID, err := s.auth.SignAndBase64Encode(userSession.ID)
if err != nil {
return nil, err
}

// save the session in the store
err = s.store.SaveUserSession(userSession)
if err != nil {
return nil, err
}

// set the session on the responseWriter
return userSession, s.transport.SetSessionOnResponse(signedSessionID, userSession, w)
}

// 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 (s *Service) ClearUserSession(userSession *user.Session, w http.ResponseWriter) *sessionerrs.Custom {
// delete the session from the store
err := s.store.DeleteUserSession(userSession.ID)
if err != nil {
return err
}

// delete the session from the response
return s.transport.DeleteSessionFromResponse(w)
}

// 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 (s *Service) GetUserSession(r *http.Request) (*user.Session, *sessionerrs.Custom) {
// read the session from the request
signedSessionID, err := s.transport.FetchSessionFromRequest(r)
if err != nil {
return nil, err
}

// decode the signedSessionID
sessionID, err := s.auth.VerifyAndDecode(signedSessionID)
if err != nil {
return nil, err
}

// try fetching a valid session from the store
return s.store.FetchValidUserSession(sessionID)
}

// ExtendUserSession extends the ExpiresAt of a session by the Options.ExpirationDuration
func (s *Service) ExtendUserSession(userSession *user.Session, r *http.Request, w http.ResponseWriter) *sessionerrs.Custom {
newExpiresAt := time.Now().Add(s.options.ExpirationDuration).UTC()

// update the provided user session
userSession.ExpiresAt = newExpiresAt

// save the session in the store with the extended expiry
err := s.store.SaveUserSession(userSession)
if err != nil {
return err
}

// fetch the signed session id from the request
signedSessionID, err := s.transport.FetchSessionFromRequest(r)
if err != nil {
return err
}

// finally, set the session on the responseWriter
return s.transport.SetSessionOnResponse(signedSessionID, userSession, w)
}
6 changes: 3 additions & 3 deletions sessions_interface.go → service_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"github.com/adam-hanna/sessions/user"
)

// ServiceInterface is used to track state in an http application
// ServiceInterface defines the methods performed by the session service
type ServiceInterface interface {
IssueUserSession(userID string, w http.ResponseWriter) (*user.Session, *sessionerrs.Custom)
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)
RefreshUserSession(userSession *user.Session, w http.ResponseWriter) *sessionerrs.Custom
ExtendUserSession(userSession *user.Session, r *http.Request, w http.ResponseWriter) *sessionerrs.Custom
}
13 changes: 13 additions & 0 deletions service_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package sessions

// setDefaultOptions sets default values for nil fields
// note @adam-hanna: this utility function should be improved. The fields and types of the options struct \
// should not be hardcoded!
func setDefaultOptions(options *Options) {
emptyOptions := Options{}
if options.ExpirationDuration == emptyOptions.ExpirationDuration {
options.ExpirationDuration = DefaultExpirationDuration
}

return
}
5 changes: 3 additions & 2 deletions sessionerrs/sessionerrs.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package sessionerrs

// Custom provides a sessions error and also the type of http status code to return
// Custom is the error type returned by this session service package.
// This custom error is useful for calling funcs to determine which http status code to return to clients on err
type Custom struct {
// Code corresponds to http status codes (e.g. 401 Unauthorized)
// Code corresponds to an http status code (e.g. 401 Unauthorized)
Code int
// Err is the actual error thrown
Err error
Expand Down
Loading

0 comments on commit 0953482

Please sign in to comment.