1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-28 08:58:38 +02:00

Move expiry module

- Remove the errors from User interfaces
This commit is contained in:
Aaron L 2018-02-14 14:18:03 -08:00
parent 726204d809
commit 23e1e849d3
7 changed files with 236 additions and 225 deletions

View File

@ -124,7 +124,7 @@ func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*htt
return r, nil
}
ctx := context.WithValue(r.Context(), ctxKeySessionState, state)
ctx := context.WithValue(r.Context(), CTXKeySessionState, state)
r = r.WithContext(ctx)
}
if a.Storage.CookieState != nil {
@ -134,7 +134,7 @@ func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*htt
} else if state == nil {
return r, nil
}
ctx := context.WithValue(r.Context(), ctxKeyCookieState, state)
ctx := context.WithValue(r.Context(), CTXKeyCookieState, state)
r = r.WithContext(ctx)
}
@ -200,7 +200,7 @@ func (c *ClientStateResponseWriter) putClientState() error {
}
if c.sessionState != nil && len(c.sessionStateEvents) > 0 {
sessionStateIntf := c.ctx.Value(ctxKeySessionState)
sessionStateIntf := c.ctx.Value(CTXKeySessionState)
var session ClientState
if sessionStateIntf != nil {
@ -213,7 +213,7 @@ func (c *ClientStateResponseWriter) putClientState() error {
}
}
if c.cookieState != nil && len(c.cookieStateEvents) > 0 {
cookieStateIntf := c.ctx.Value(ctxKeyCookieState)
cookieStateIntf := c.ctx.Value(CTXKeyCookieState)
var cookie ClientState
if cookieStateIntf != nil {
@ -231,43 +231,43 @@ func (c *ClientStateResponseWriter) putClientState() error {
// PutSession puts a value into the session
func PutSession(w http.ResponseWriter, key, val string) {
putState(w, ctxKeySessionState, key, val)
putState(w, CTXKeySessionState, key, val)
}
// DelSession deletes a key-value from the session.
func DelSession(w http.ResponseWriter, key string) {
delState(w, ctxKeySessionState, key)
delState(w, CTXKeySessionState, key)
}
// GetSession fetches a value from the session
func GetSession(r *http.Request, key string) (string, bool) {
return getState(r, ctxKeySessionState, key)
return getState(r, CTXKeySessionState, key)
}
// PutCookie puts a value into the session
func PutCookie(w http.ResponseWriter, key, val string) {
putState(w, ctxKeyCookieState, key, val)
putState(w, CTXKeyCookieState, key, val)
}
// DelCookie deletes a key-value from the session.
func DelCookie(w http.ResponseWriter, key string) {
delState(w, ctxKeyCookieState, key)
delState(w, CTXKeyCookieState, key)
}
// GetCookie fetches a value from the session
func GetCookie(r *http.Request, key string) (string, bool) {
return getState(r, ctxKeyCookieState, key)
return getState(r, CTXKeyCookieState, key)
}
func putState(w http.ResponseWriter, ctxKey contextKey, key, val string) {
setState(w, ctxKey, ClientStateEventPut, key, val)
func putState(w http.ResponseWriter, CTXKey contextKey, key, val string) {
setState(w, CTXKey, ClientStateEventPut, key, val)
}
func delState(w http.ResponseWriter, ctxKey contextKey, key string) {
setState(w, ctxKey, ClientStateEventDel, key, "")
func delState(w http.ResponseWriter, CTXKey contextKey, key string) {
setState(w, CTXKey, ClientStateEventDel, key, "")
}
func setState(w http.ResponseWriter, ctxKey contextKey, op ClientStateEventKind, key, val string) {
func setState(w http.ResponseWriter, CTXKey contextKey, op ClientStateEventKind, key, val string) {
csrw := MustClientStateResponseWriter(w)
ev := ClientStateEvent{
Kind: op,
@ -278,16 +278,16 @@ func setState(w http.ResponseWriter, ctxKey contextKey, op ClientStateEventKind,
ev.Value = val
}
switch ctxKey {
case ctxKeySessionState:
switch CTXKey {
case CTXKeySessionState:
csrw.sessionStateEvents = append(csrw.sessionStateEvents, ev)
case ctxKeyCookieState:
case CTXKeyCookieState:
csrw.cookieStateEvents = append(csrw.cookieStateEvents, ev)
}
}
func getState(r *http.Request, ctxKey contextKey, key string) (string, bool) {
val := r.Context().Value(ctxKey)
func getState(r *http.Request, CTXKey contextKey, key string) (string, bool) {
val := r.Context().Value(CTXKey)
if val == nil {
return "", false
}

View File

@ -7,12 +7,13 @@ import (
type contextKey string
// CTX Keys for authboss
const (
ctxKeyPID contextKey = "pid"
ctxKeyUser contextKey = "user"
CTXKeyPID contextKey = "pid"
CTXKeyUser contextKey = "user"
ctxKeySessionState contextKey = "session"
ctxKeyCookieState contextKey = "cookie"
CTXKeySessionState contextKey = "session"
CTXKeyCookieState contextKey = "cookie"
// CTXKeyData is a context key for the accumulating
// map[string]interface{} (authboss.HTMLData) to pass to the
@ -26,7 +27,7 @@ func (c contextKey) String() string {
// CurrentUserID retrieves the current user from the session.
func (a *Authboss) CurrentUserID(w http.ResponseWriter, r *http.Request) (string, error) {
if pid := r.Context().Value(ctxKeyPID); pid != nil {
if pid := r.Context().Value(CTXKeyPID); pid != nil {
return pid.(string), nil
}
@ -49,7 +50,7 @@ func (a *Authboss) CurrentUserIDP(w http.ResponseWriter, r *http.Request) string
// CurrentUser retrieves the current user from the session and the database.
func (a *Authboss) CurrentUser(w http.ResponseWriter, r *http.Request) (User, error) {
if user := r.Context().Value(ctxKeyUser); user != nil {
if user := r.Context().Value(CTXKeyUser); user != nil {
return user.(User), nil
}
@ -88,7 +89,7 @@ func (a *Authboss) currentUser(ctx context.Context, pid string) (User, error) {
// change the current method's request pointer itself to the new request that
// contains the new context that has the pid in it.
func (a *Authboss) LoadCurrentUserID(w http.ResponseWriter, r **http.Request) (string, error) {
if pid := (*r).Context().Value(ctxKeyPID); pid != nil {
if pid := (*r).Context().Value(CTXKeyPID); pid != nil {
return pid.(string), nil
}
@ -101,7 +102,7 @@ func (a *Authboss) LoadCurrentUserID(w http.ResponseWriter, r **http.Request) (s
return "", nil
}
ctx := context.WithValue((**r).Context(), ctxKeyPID, pid)
ctx := context.WithValue((**r).Context(), CTXKeyPID, pid)
*r = (**r).WithContext(ctx)
return pid, nil
@ -124,7 +125,7 @@ func (a *Authboss) LoadCurrentUserIDP(w http.ResponseWriter, r **http.Request) s
// contains the new context that has the user in it. Calls LoadCurrentUserID
// so the primary id is also put in the context.
func (a *Authboss) LoadCurrentUser(w http.ResponseWriter, r **http.Request) (User, error) {
if user := (*r).Context().Value(ctxKeyUser); user != nil {
if user := (*r).Context().Value(CTXKeyUser); user != nil {
return user.(User), nil
}
@ -143,7 +144,7 @@ func (a *Authboss) LoadCurrentUser(w http.ResponseWriter, r **http.Request) (Use
return nil, err
}
ctx = context.WithValue(ctx, ctxKeyUser, user)
ctx = context.WithValue(ctx, CTXKeyUser, user)
*r = (**r).WithContext(ctx)
return user, nil
}

View File

@ -1,9 +1,12 @@
package authboss
// Package expire helps expire user's logged in sessions
package expire
import (
"context"
"net/http"
"time"
"github.com/volatiletech/authboss"
)
var nowTime = time.Now
@ -15,7 +18,7 @@ func TimeToExpiry(r *http.Request, expireAfter time.Duration) time.Duration {
}
func timeToExpiry(r *http.Request, expireAfter time.Duration) time.Duration {
dateStr, ok := GetSession(r, SessionLastAction)
dateStr, ok := authboss.GetSession(r, authboss.SessionLastAction)
if !ok {
return expireAfter
}
@ -40,33 +43,33 @@ func RefreshExpiry(w http.ResponseWriter, r *http.Request) {
}
func refreshExpiry(w http.ResponseWriter) {
PutSession(w, SessionLastAction, nowTime().UTC().Format(time.RFC3339))
authboss.PutSession(w, authboss.SessionLastAction, nowTime().UTC().Format(time.RFC3339))
}
type expireMiddleware struct {
ab *Authboss
next http.Handler
expireAfter time.Duration
next http.Handler
}
// ExpireMiddleware ensures that the user's expiry information is kept up-to-date
// Middleware ensures that the user's expiry information is kept up-to-date
// on each request. Deletes the SessionKey from the session if the user is
// expired (a.ExpireAfter duration since SessionLastAction).
// This middleware conflicts with use of the Remember module, don't enable both
// at the same time.
func (a *Authboss) ExpireMiddleware(next http.Handler) http.Handler {
return expireMiddleware{a, next}
func Middleware(ab *authboss.Authboss, next http.Handler) http.Handler {
return expireMiddleware{ab.Config.Modules.ExpireAfter, next}
}
// ServeHTTP removes the session and hides the loaded user from the handlers
// below it.
func (m expireMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if _, ok := GetSession(r, SessionKey); ok {
ttl := timeToExpiry(r, m.ab.Modules.ExpireAfter)
if _, ok := authboss.GetSession(r, authboss.SessionKey); ok {
ttl := timeToExpiry(r, m.expireAfter)
if ttl == 0 {
DelSession(w, SessionKey)
DelSession(w, SessionLastAction)
ctx := context.WithValue(r.Context(), ctxKeyPID, nil)
ctx = context.WithValue(ctx, ctxKeyUser, nil)
authboss.DelSession(w, authboss.SessionKey)
authboss.DelSession(w, authboss.SessionLastAction)
ctx := context.WithValue(r.Context(), authboss.CTXKeyPID, nil)
ctx = context.WithValue(ctx, authboss.CTXKeyUser, nil)
r = r.WithContext(ctx)
} else {
refreshExpiry(w)

154
expire/expire_test.go Normal file
View File

@ -0,0 +1,154 @@
package expire
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/internal/mocks"
)
func TestExpireIsExpired(t *testing.T) {
ab := authboss.New()
clientRW := mocks.NewClientRW()
clientRW.ClientValues[authboss.SessionKey] = "username"
clientRW.ClientValues[authboss.SessionLastAction] = time.Now().UTC().Format(time.RFC3339)
ab.Storage.SessionState = clientRW
r := httptest.NewRequest("GET", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyPID, "primaryid"))
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, struct{}{}))
w := ab.NewResponse(httptest.NewRecorder(), r)
r, err := ab.LoadClientState(w, r)
if err != nil {
t.Error(err)
}
// No t.Parallel() - Also must be after refreshExpiry() call
nowTime = func() time.Time {
return time.Now().UTC().Add(time.Hour * 2)
}
defer func() {
nowTime = time.Now
}()
called := false
hadUser := false
m := Middleware(ab, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
if r.Context().Value(authboss.CTXKeyPID) != nil {
hadUser = true
}
if r.Context().Value(authboss.CTXKeyUser) != nil {
hadUser = true
}
}))
m.ServeHTTP(w, r)
if !called {
t.Error("expected middleware to call handler")
}
if hadUser {
t.Error("expected user not to be present")
}
w.WriteHeader(200)
if _, ok := clientRW.ClientValues[authboss.SessionKey]; ok {
t.Error("this key should have been deleted\n", clientRW)
}
if _, ok := clientRW.ClientValues[authboss.SessionLastAction]; ok {
t.Error("this key should have been deleted\n", clientRW)
}
}
func TestExpireNotExpired(t *testing.T) {
ab := authboss.New()
clientRW := mocks.NewClientRW()
clientRW.ClientValues[authboss.SessionKey] = "username"
clientRW.ClientValues[authboss.SessionLastAction] = time.Now().UTC().Format(time.RFC3339)
ab.Storage.SessionState = clientRW
var err error
r := httptest.NewRequest("GET", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyPID, "primaryid"))
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, struct{}{}))
w := ab.NewResponse(httptest.NewRecorder(), r)
r, err = ab.LoadClientState(w, r)
if err != nil {
t.Error(err)
}
// No t.Parallel() - Also must be after refreshExpiry() call
newTime := time.Now().UTC().Add(ab.Modules.ExpireAfter / 2)
nowTime = func() time.Time {
return newTime
}
defer func() {
nowTime = time.Now
}()
called := false
hadUser := true
m := Middleware(ab, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
if r.Context().Value(authboss.CTXKeyPID) == nil {
hadUser = false
}
if r.Context().Value(authboss.CTXKeyUser) == nil {
hadUser = false
}
}))
m.ServeHTTP(w, r)
if !called {
t.Error("expected middleware to call handler")
}
if !hadUser {
t.Error("expected user to be present")
}
want := newTime.Format(time.RFC3339)
w.WriteHeader(200)
if last, ok := clientRW.ClientValues[authboss.SessionLastAction]; !ok {
t.Error("this key should be present", clientRW)
} else if want != last {
t.Error("want:", want, "got:", last)
}
}
func TestExpireTimeToExpiry(t *testing.T) {
t.Parallel()
r := httptest.NewRequest("GET", "/", nil)
want := 5 * time.Second
dur := TimeToExpiry(r, want)
if dur != want {
t.Error("duration was wrong:", dur)
}
}
func TestExpireRefreshExpiry(t *testing.T) {
t.Parallel()
ab := authboss.New()
clientRW := mocks.NewClientRW()
ab.Storage.SessionState = clientRW
r := httptest.NewRequest("GET", "/", nil)
w := ab.NewResponse(httptest.NewRecorder(), r)
RefreshExpiry(w, r)
w.WriteHeader(200)
if _, ok := clientRW.ClientValues[authboss.SessionLastAction]; !ok {
t.Error("this key should have been set")
}
}

View File

@ -1,160 +0,0 @@
package authboss
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestExpireIsExpired(t *testing.T) {
ab := New()
ab.Storage.SessionState = newMockClientStateRW(
SessionKey, "username",
SessionLastAction, time.Now().UTC().Format(time.RFC3339),
)
r := httptest.NewRequest("GET", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), ctxKeyPID, "primaryid"))
r = r.WithContext(context.WithValue(r.Context(), ctxKeyUser, struct{}{}))
w := ab.NewResponse(httptest.NewRecorder(), r)
r, err := ab.LoadClientState(w, r)
if err != nil {
t.Error(err)
}
// No t.Parallel() - Also must be after refreshExpiry() call
nowTime = func() time.Time {
return time.Now().UTC().Add(ab.Modules.ExpireAfter * 2)
}
defer func() {
nowTime = time.Now
}()
called := false
hadUser := false
m := ab.ExpireMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
if r.Context().Value(ctxKeyPID) != nil {
hadUser = true
}
if r.Context().Value(ctxKeyUser) != nil {
hadUser = true
}
}))
m.ServeHTTP(w, r)
if !called {
t.Error("expected middleware to call handler")
}
if hadUser {
t.Error("expected user not to be present")
}
want := ClientStateEvent{
Kind: ClientStateEventDel,
Key: SessionKey,
}
if got := w.sessionStateEvents[0]; got != want {
t.Error("want:", want, "got:", got)
}
want = ClientStateEvent{
Kind: ClientStateEventDel,
Key: SessionLastAction,
}
if got := w.sessionStateEvents[1]; got != want {
t.Error("want:", want, "got:", got)
}
}
func TestExpireNotExpired(t *testing.T) {
ab := New()
ab.Config.Modules.ExpireAfter = time.Hour
ab.Storage.SessionState = newMockClientStateRW(
SessionKey, "username",
SessionLastAction, time.Now().UTC().Format(time.RFC3339),
)
var err error
r := httptest.NewRequest("GET", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), ctxKeyPID, "primaryid"))
r = r.WithContext(context.WithValue(r.Context(), ctxKeyUser, struct{}{}))
w := ab.NewResponse(httptest.NewRecorder(), r)
r, err = ab.LoadClientState(w, r)
if err != nil {
t.Error(err)
}
// No t.Parallel() - Also must be after refreshExpiry() call
newTime := time.Now().UTC().Add(ab.Modules.ExpireAfter / 2)
nowTime = func() time.Time {
return newTime
}
defer func() {
nowTime = time.Now
}()
called := false
hadUser := true
m := ab.ExpireMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
if r.Context().Value(ctxKeyPID) == nil {
hadUser = false
}
if r.Context().Value(ctxKeyUser) == nil {
hadUser = false
}
}))
m.ServeHTTP(w, r)
if !called {
t.Error("expected middleware to call handler")
}
if !hadUser {
t.Error("expected user to be present")
}
want := ClientStateEvent{
Kind: ClientStateEventPut,
Key: SessionLastAction,
Value: newTime.Format(time.RFC3339),
}
if got := w.sessionStateEvents[0]; got != want {
t.Error("want:", want, "got:", got)
}
}
func TestExpireTimeToExpiry(t *testing.T) {
t.Parallel()
r := httptest.NewRequest("GET", "/", nil)
want := 5 * time.Second
dur := TimeToExpiry(r, want)
if dur != want {
t.Error("duration was wrong:", dur)
}
}
func TestExpireRefreshExpiry(t *testing.T) {
t.Parallel()
ab := New()
r := httptest.NewRequest("GET", "/", nil)
w := ab.NewResponse(httptest.NewRecorder(), r)
RefreshExpiry(w, r)
if got := w.sessionStateEvents[0].Kind; got != ClientStateEventPut {
t.Error("wrong event:", got)
}
if got := w.sessionStateEvents[0].Key; got != SessionLastAction {
t.Error("wrong key:", got)
}
}

View File

@ -278,6 +278,15 @@ func (c *ClientStateRW) ReadState(http.ResponseWriter, *http.Request) (authboss.
// WriteState to memory
func (c *ClientStateRW) WriteState(w http.ResponseWriter, cstate authboss.ClientState, cse []authboss.ClientStateEvent) error {
for _, e := range cse {
switch e.Kind {
case authboss.ClientStateEventPut:
c.ClientValues[e.Key] = e.Value
case authboss.ClientStateEventDel:
delete(c.ClientValues, e.Key)
}
}
return nil
}

View File

@ -48,28 +48,32 @@ type ServerStorer interface {
// User has a PID (primary ID) that is used on the site as
// a single unique identifier to any given user (very typically e-mail
// or username).
//
// User interfaces return no errors or bools to signal that a value was
// not present. Instead 0-value = null = not present, this puts the onus
// on Authboss code to check for this.
type User interface {
GetPID(ctx context.Context) (pid string, err error)
PutPID(ctx context.Context, pid string) error
GetPID(ctx context.Context) (pid string)
PutPID(ctx context.Context, pid string)
}
// AuthableUser is identified by a password
type AuthableUser interface {
User
GetPassword(ctx context.Context) (password string, err error)
PutPassword(ctx context.Context, password string) error
GetPassword(ctx context.Context) (password string)
PutPassword(ctx context.Context, password string)
}
// ConfirmableUser can be in a state of confirmed or not
type ConfirmableUser interface {
User
GetConfirmed(ctx context.Context) (confirmed bool, err error)
GetConfirmToken(ctx context.Context) (token string, err error)
GetConfirmed(ctx context.Context) (confirmed bool)
GetConfirmToken(ctx context.Context) (token string)
PutConfirmed(ctx context.Context, confirmed bool) error
PutConfirmToken(ctx context.Context, token string) error
PutConfirmed(ctx context.Context, confirmed bool)
PutConfirmToken(ctx context.Context, token string)
}
// ArbitraryUser allows arbitrary data from the web form through. You should
@ -80,10 +84,10 @@ type ArbitraryUser interface {
// GetArbitrary is used only to display the arbitrary data back to the user
// when the form is reset.
GetArbitrary(ctx context.Context) (arbitrary map[string]string, err error)
GetArbitrary(ctx context.Context) (arbitrary map[string]string)
// PutArbitrary allows arbitrary fields defined by the authboss library
// consumer to add fields to the user registration piece.
PutArbitrary(ctx context.Context, arbitrary map[string]string) error
PutArbitrary(ctx context.Context, arbitrary map[string]string)
}
// OAuth2User allows reading and writing values relating to OAuth2
@ -92,19 +96,19 @@ type OAuth2User interface {
// IsOAuth2User checks to see if a user was registered in the site as an
// oauth2 user.
IsOAuth2User(ctx context.Context) (bool, error)
IsOAuth2User(ctx context.Context) bool
GetUID(ctx context.Context) (uid string, err error)
GetProvider(ctx context.Context) (provider string, err error)
GetToken(ctx context.Context) (token string, err error)
GetRefreshToken(ctx context.Context) (refreshToken string, err error)
GetExpiry(ctx context.Context) (expiry time.Duration, err error)
GetUID(ctx context.Context) (uid string)
GetProvider(ctx context.Context) (provider string)
GetToken(ctx context.Context) (token string)
GetRefreshToken(ctx context.Context) (refreshToken string)
GetExpiry(ctx context.Context) (expiry time.Duration)
PutUID(ctx context.Context, uid string) error
PutProvider(ctx context.Context, provider string) error
PutToken(ctx context.Context, token string) error
PutRefreshToken(ctx context.Context, refreshToken string) error
PutExpiry(ctx context.Context, expiry time.Duration) error
PutUID(ctx context.Context, uid string)
PutProvider(ctx context.Context, provider string)
PutToken(ctx context.Context, token string)
PutRefreshToken(ctx context.Context, refreshToken string)
PutExpiry(ctx context.Context, expiry time.Duration)
}
// MustBeAuthable forces an upgrade conversion to Authable