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:
parent
59454bf909
commit
7f9fe3ec77
47
callbacks.go
47
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)
|
||||
}
|
||||
}
|
||||
|
38
callbacks_test.go
Normal file
38
callbacks_test.go
Normal 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.")
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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{}
|
||||
}
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user