1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-03-21 21:47:13 +02:00

Add session and cookie concepts.

- Add tests for callbacks.
- Refactor callbacks into a keyed map.
This commit is contained in:
Aaron 2015-01-12 14:02:07 -08:00
parent 59454bf909
commit 7f9fe3ec77
9 changed files with 301 additions and 49 deletions

View File

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

38
callbacks_test.go Normal file
View File

@ -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.")
}
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

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

View File

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

View File

@ -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{}
}

View File

@ -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.