From 7f9fe3ec7785f20a70127f31f86d66fc1fd193df Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 12 Jan 2015 14:02:07 -0800 Subject: [PATCH] Add session and cookie concepts. - Add tests for callbacks. - Refactor callbacks into a keyed map. --- callbacks.go | 47 +++++++++---- callbacks_test.go | 38 +++++++++++ client_storer.go | 21 +++++- config.go | 8 +-- context.go | 7 +- remember/remember.go | 82 +++++++++++++++++++---- remember/remember_test.go | 134 ++++++++++++++++++++++++++++++++++++-- session_storer.go | 9 --- storer.go | 4 +- 9 files changed, 301 insertions(+), 49 deletions(-) create mode 100644 callbacks_test.go delete mode 100644 session_storer.go diff --git a/callbacks.go b/callbacks.go index 12234b7..fe06557 100644 --- a/callbacks.go +++ b/callbacks.go @@ -1,36 +1,53 @@ package authboss +// Event is used for callback registration. +type Event int + +// These are the events that are available for use. +const ( + EventRegister Event = iota + EventAuth +) + // Before callbacks can interrupt the flow by returning an error. This is used to stop // the callback chain and the original handler from executing. -type Before func(Context) error +type Before func(*Context) error // After is a request callback that happens after the event. -type After func(Context) +type After func(*Context) // Callbacks is a collection of callbacks that fire before and after certain // methods. type Callbacks struct { - beforeAuth []Before - afterAuth []After + before map[Event][]Before + after map[Event][]After } func NewCallbacks() *Callbacks { return &Callbacks{ - make([]Before, 0), - make([]After, 0), + make(map[Event][]Before), + make(map[Event][]After), } } -func (c *Callbacks) AddBeforeAuth(f Before) { - c.beforeAuth = append(c.beforeAuth, f) +// Before event, call callback. +func (c *Callbacks) Before(e Event, f Before) { + callbacks := c.before[e] + callbacks = append(callbacks, f) + c.before[e] = callbacks } -func (c *Callbacks) AddAfterAuth(f After) { - c.afterAuth = append(c.afterAuth, f) +// After event, call callback. +func (c *Callbacks) After(e Event, f After) { + callbacks := c.after[e] + callbacks = append(callbacks, f) + c.after[e] = callbacks } -func (c *Callbacks) BeforeAuth(ctx Context) error { - for _, fn := range c.beforeAuth { +// FireBefore event to all the callbacks with a context. +func (c *Callbacks) FireBefore(e Event, ctx *Context) error { + callbacks := c.before[e] + for _, fn := range callbacks { err := fn(ctx) if err != nil { return err @@ -40,8 +57,10 @@ func (c *Callbacks) BeforeAuth(ctx Context) error { return nil } -func (c *Callbacks) AfterAuth(ctx Context) { - for _, fn := range c.afterAuth { +// FireAfter event to all the callbacks with a context. +func (c *Callbacks) FireAfter(e Event, ctx *Context) { + callbacks := c.after[e] + for _, fn := range callbacks { fn(ctx) } } diff --git a/callbacks_test.go b/callbacks_test.go new file mode 100644 index 0000000..244ee2b --- /dev/null +++ b/callbacks_test.go @@ -0,0 +1,38 @@ +package authboss + +import "testing" + +func TestCallbacks(t *testing.T) { + afterCalled := false + beforeCalled := false + c := NewCallbacks() + + c.Before(EventRegister, func(ctx *Context) error { + beforeCalled = true + return nil + }) + c.After(EventRegister, func(ctx *Context) { + afterCalled = true + }) + + if beforeCalled || afterCalled { + t.Error("Neither should be called.") + } + + err := c.FireBefore(EventRegister, NewContext()) + if err != nil { + t.Error("Unexpected error:", err) + } + + if !beforeCalled { + t.Error("Expected before to have been called.") + } + if afterCalled { + t.Error("Expected after not to be called.") + } + + c.FireAfter(EventRegister, NewContext()) + if !afterCalled { + t.Error("Expected after to be called.") + } +} diff --git a/client_storer.go b/client_storer.go index 7feb5d2..d61d0a4 100644 --- a/client_storer.go +++ b/client_storer.go @@ -1,8 +1,23 @@ package authboss -// ClientStorer should be able to store values on the clients machine. This is -// usually going to be a cookie store. +import "net/http" + +// SessionKey is the primarily used key by authboss. +const SessionKey = "uid" + +// ClientStorer should be able to store values on the clients machine. Cookie and +// Session storers are built with this interface. type ClientStorer interface { Put(key, value string) - Get(key string) string + Get(key string) (string, bool) } + +// CookieStoreMaker is used to create a cookie storer from an http request. Keep in mind +// security considerations for your implementation, Secure, HTTP-Only, etc flags. +type CookieStoreMaker func(*http.Request) ClientStorer + +// SessionStoreMaker is used to create a session storer from an http request. +// It must be implemented to satisfy certain modules (auth, remember primarily). +// It should be a secure storage of the session. This means if it represents a cookie-based session +// storage these cookies should be signed in order to prevent tampering, or they should be encrypted. +type SessionStoreMaker func(*http.Request) ClientStorer diff --git a/config.go b/config.go index 414331c..83eec9d 100644 --- a/config.go +++ b/config.go @@ -13,10 +13,10 @@ type Config struct { AuthLogoutRoute string `json:"authLogoutRoute" xml:"authLogoutRoute"` AuthLoginSuccessRoute string `json:"authLoginSuccessRoute" xml:"authLoginSuccessRoute"` - Storer Storer `json:"-" xml:"-"` - ClientStorer ClientStorer `json:"-" xml:"-"` - SessionStorer SessionStorer `json:"-" xml:"-"` - LogWriter io.Writer `json:"-" xml:"-"` + Storer Storer `json:"-" xml:"-"` + CookieStoreMaker CookieStoreMaker `json:"-" xml:"-"` + SessionStoreMaker SessionStoreMaker `json:"-" xml:"-"` + LogWriter io.Writer `json:"-" xml:"-"` } // NewConfig creates a new config full of default values ready to override. diff --git a/context.go b/context.go index c3b7040..70de7cc 100644 --- a/context.go +++ b/context.go @@ -4,8 +4,9 @@ package authboss // need for context is a request's session store. It is not safe for use by // multiple goroutines. type Context struct { - ClientStorer ClientStorer - User Attributes + SessionStorer ClientStorer + CookieStorer ClientStorer + User Attributes keyValues map[string]interface{} } @@ -16,10 +17,12 @@ func NewContext() *Context { } } +// Put an arbitrary key-value into the context. func (c *Context) Put(key string, thing interface{}) { c.keyValues[key] = thing } +// Get an arbitrary key-value from the context. func (c *Context) Get(key string) (thing interface{}, ok bool) { thing, ok = c.keyValues[key] return thing, ok diff --git a/remember/remember.go b/remember/remember.go index 325bf06..3912021 100644 --- a/remember/remember.go +++ b/remember/remember.go @@ -4,14 +4,26 @@ package remember import ( + "bytes" "crypto/md5" "crypto/rand" "encoding/base64" "errors" + "fmt" + "io" "gopkg.in/authboss.v0" ) +const ( + // ValueKey is used for cookies and form input names. + ValueKey = "rm" + // HalfAuthKey is used for sessions that have been authenticated by + // the remember module. This serves as a way to force full authentication + // by denying half-authed users acccess to sensitive areas. + HalfAuthKey = "halfauth" +) + const nRandBytes = 32 // R is the singleton instance of the remember module which will have been @@ -24,16 +36,25 @@ func init() { } type Remember struct { - storer authboss.TokenStorer + storer authboss.TokenStorer + cookieStorer authboss.ClientStorer + sessionStorer authboss.ClientStorer + logger io.Writer } func (r *Remember) Initialize(c *authboss.Config) error { + if c.Storer == nil { + return errors.New("remember: Need a TokenStorer.") + } + if storer, ok := c.Storer.(authboss.TokenStorer); !ok { - return errors.New("Remember module requires a TokenStorer interface be satisfied.") + return errors.New("remember: TokenStorer required for remember me functionality.") } else { r.storer = storer } + r.logger = c.LogWriter + return nil } @@ -45,27 +66,47 @@ func (r *Remember) Storage() authboss.StorageOptions { return nil } +// AfterAuth is called after authentication is successful. +func (r *Remember) AfterAuth(ctx *authboss.Context) { + if val, ok := ctx.Get(ValueKey); ok && val != "true" { + return + } + + /*if err := ctx.LoadUser(r.storer); err != nil { + fmt.Fprintln(r.logger, "remember: Failed to load user:", err) + return + }*/ + + key := ctx.User["Username"].Value.(string) + if _, err := r.New(ctx.CookieStorer, key); err != nil { + fmt.Fprintf(r.logger, "Failed to create remember token: %v", err) + } +} + // New generates a new remember token and stores it in the configured TokenStorer. // The return value is a token that should only be given to a user if the delivery // method is secure which means at least signed if not encrypted. -func (r *Remember) New(ctx *authboss.Context, storageKey string, keys ...string) (string, error) { - token := make([]byte, nRandBytes) - if _, err := rand.Read(token); err != nil { - return "", err - } +func (r *Remember) New(cstorer authboss.ClientStorer, storageKey string) (string, error) { + token := make([]byte, nRandBytes+len(storageKey)+1) + copy(token, []byte(storageKey)) + token[len(storageKey)] = ';' - for _, k := range keys { - token = append(token, []byte(k)...) + if _, err := rand.Read(token[len(storageKey)+1:]); err != nil { + return "", err } sum := md5.Sum(token) finalToken := base64.URLEncoding.EncodeToString(token) storageToken := base64.StdEncoding.EncodeToString(sum[:]) + // Save the token in the DB if err := r.storer.AddToken(storageKey, storageToken); err != nil { return "", err } + // Write the finalToken to the cookie + cstorer.Put(ValueKey, finalToken) + return finalToken, nil } @@ -73,19 +114,38 @@ func (r *Remember) New(ctx *authboss.Context, storageKey string, keys ...string) // is matching in the database. If something is found the old token is deleted // and a new one should be generated. The return value is the key of the // record who owned this token. -func (r *Remember) Auth(ctx *authboss.Context, finalToken string) (string, error) { +func (r *Remember) Auth( + cstorer authboss.ClientStorer, + sstorer authboss.ClientStorer, + finalToken string) (string, error) { + token, err := base64.URLEncoding.DecodeString(finalToken) if err != nil { return "", err } + index := bytes.IndexByte(token, ';') + if index < 0 { + return "", errors.New("remember: Invalid remember me token.") + } + + // Get the key. + givenKey := token[:index] + + // Verify the tokens match. sum := md5.Sum(token) - key, err := r.storer.UseToken(base64.StdEncoding.EncodeToString(sum[:])) + + key, err := r.storer.UseToken(string(givenKey), base64.StdEncoding.EncodeToString(sum[:])) if err == authboss.TokenNotFound { return "", nil } else if err != nil { return "", err } + // Ensure a half-auth. + sstorer.Put(HalfAuthKey, "true") + // Log the user in. + sstorer.Put(authboss.SessionKey, key) + return key, nil } diff --git a/remember/remember_test.go b/remember/remember_test.go index e84c236..48022be 100644 --- a/remember/remember_test.go +++ b/remember/remember_test.go @@ -6,11 +6,137 @@ import ( "gopkg.in/authboss.v0" ) -func TestMakeToken(t *testing.T) { - tok, err := R.New(authboss.NewContext(), "storage", "hello", "world", "5") +type testClientStorer map[string]string + +func (t testClientStorer) Put(key, value string) { + t[key] = value +} + +func (t testClientStorer) Get(key string) (string, bool) { + s, ok := t[key] + return s, ok +} + +type testStorer struct { +} + +func (t testStorer) Create(key string, attr authboss.Attributes) error { return nil } +func (t testStorer) Put(key string, attr authboss.Attributes) error { return nil } +func (t testStorer) Get(key string, attrMeta authboss.AttributeMeta) (interface{}, error) { + return nil, nil +} + +type testTokenStorer struct { + testStorer + key string + token string +} + +func (t *testTokenStorer) AddToken(key, token string) error { + t.key = key + t.token = token + return nil +} +func (t *testTokenStorer) DelTokens(key string) error { + t.key = "" + t.token = "" + return nil +} +func (t *testTokenStorer) UseToken(givenKey, token string) (key string, err error) { + if givenKey == t.key { + ret := t.key + t.key = "" + t.token = "" + return ret, nil + } + return "", authboss.TokenNotFound +} + +func TestInitialize(t *testing.T) { + testConfig := authboss.NewConfig() + + r := &Remember{} + err := r.Initialize(testConfig) + if err == nil { + t.Error("Expected error about token storers.") + } + + testConfig.Storer = testStorer{} + err = r.Initialize(testConfig) + if err == nil { + t.Error("Expected error about token storers.") + } + + testConfig.Storer = &testTokenStorer{} + err = r.Initialize(testConfig) if err != nil { t.Error("Unexpected error:", err) - } else if len(tok) == 0 { - t.Error("It should have made a token.") + } +} + +func TestAfterAuth(t *testing.T) { + // TODO(aarondl): This + + /*ctx := authboss.NewContext() + ctx.SessionStorer = session + ctx.CookieStorer = cookies*/ +} + +func TestNew(t *testing.T) { + storer := &testTokenStorer{} + R.storer = storer + cookies := make(testClientStorer) + + key := "tester" + token, err := R.New(cookies, key) + + if err != nil { + t.Error("Unexpected error:", err) + } + + if len(token) == 0 { + t.Error("Expected a token.") + } + + if storer.key != key { + t.Error("Expected it to store against the key:", storer.key) + } + + if token != cookies[ValueKey] { + t.Error("Expected a cookie set with the token.") + } + + if len(storer.token) == 0 { + t.Error("Expected a token to be saved.") + } +} + +func TestAuth(t *testing.T) { + storer := &testTokenStorer{} + R.storer = storer + cookies := make(testClientStorer) + session := make(testClientStorer) + + key := "tester" + token, err := R.New(cookies, key) + if err != nil { + t.Error("Unexpected error:", err) + } + + outKey, err := R.Auth(cookies, session, token) + if err != nil { + t.Error("Unexpected error:", err) + } + + if session[HalfAuthKey] != "true" { + t.Error("The user should have been half-authed.") + } + + if session[authboss.SessionKey] != key { + t.Error("The user should have been logged in.") + } + + if key != outKey { + t.Error("Keys should have matched:", outKey) } } diff --git a/session_storer.go b/session_storer.go deleted file mode 100644 index 136eb3e..0000000 --- a/session_storer.go +++ /dev/null @@ -1,9 +0,0 @@ -package authboss - -// SessionStorer must be implemented to satisfy certain modules (auth, remember primarily). -// It should be a secure storage of the session. This means if it represents a cookie storage -// these cookies should be signed in order to prevent tampering, or they should be encrypted. -type SessionStorer interface { - Put(key string, value interface{}) - Get(key string) interface{} -} diff --git a/storer.go b/storer.go index 11b03af..684b405 100644 --- a/storer.go +++ b/storer.go @@ -41,10 +41,10 @@ type TokenStorer interface { AddToken(key, token string) error // DelTokens removes all tokens for a given key. DelTokens(key string) error - // UseToken finds the token, removes the key/token entry in the store + // UseToken finds the key-token pair, removes the entry in the store // and returns the key that was found. If the token could not be found // return "", TokenNotFound - UseToken(token string) (key string, err error) + UseToken(givenKey, token string) (key string, err error) } // DataType represents the various types that clients must be able to store.