1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-01-08 04:03:53 +02:00
authboss/remember/remember.go
2021-02-14 22:39:57 -08:00

179 lines
5.1 KiB
Go

// Package remember implements persistent logins using cookies
package remember
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"io"
"net/http"
"github.com/friendsofgo/errors"
"github.com/volatiletech/authboss/v3"
)
const (
nNonceSize = 32
)
func init() {
authboss.RegisterModule("remember", &Remember{})
}
// Remember module
type Remember struct {
*authboss.Authboss
}
// Init module
func (r *Remember) Init(ab *authboss.Authboss) error {
r.Authboss = ab
r.Events.After(authboss.EventAuth, r.RememberAfterAuth)
r.Events.After(authboss.EventOAuth2, r.RememberAfterAuth)
r.Events.After(authboss.EventRecoverEnd, r.AfterPasswordReset)
return nil
}
// RememberAfterAuth creates a remember token and saves it in the user's cookies.
func (r *Remember) RememberAfterAuth(w http.ResponseWriter, req *http.Request, handled bool) (bool, error) {
rmIntf := req.Context().Value(authboss.CTXKeyValues)
if rmIntf == nil {
return false, nil
} else if rm, ok := rmIntf.(authboss.RememberValuer); !ok || !rm.GetShouldRemember() {
return false, nil
}
user := r.Authboss.CurrentUserP(req)
hash, token, err := GenerateToken(user.GetPID())
if err != nil {
return false, err
}
storer := authboss.EnsureCanRemember(r.Authboss.Config.Storage.Server)
if err = storer.AddRememberToken(req.Context(), user.GetPID(), hash); err != nil {
return false, err
}
authboss.PutCookie(w, authboss.CookieRemember, token)
return false, nil
}
// Middleware automatically authenticates users if they have remember me tokens
// If the user has been loaded already, it returns early
func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Safely can ignore error here
if id, _ := ab.CurrentUserID(r); len(id) == 0 {
if err := Authenticate(ab, w, &r); err != nil {
logger := ab.RequestLogger(r)
logger.Errorf("failed to authenticate user via remember me: %+v", err)
}
}
next.ServeHTTP(w, r)
})
}
}
// Authenticate the user using their remember cookie.
// If the cookie proves unusable it will be deleted. A cookie
// may be unusable for the following reasons:
// - Can't decode the base64
// - Invalid token format
// - Can't find token in DB
//
// In order to authenticate it adds to the request context as well as to the
// cookie and session states.
func Authenticate(ab *authboss.Authboss, w http.ResponseWriter, req **http.Request) error {
logger := ab.RequestLogger(*req)
cookie, ok := authboss.GetCookie(*req, authboss.CookieRemember)
if !ok {
return nil
}
rawToken, err := base64.URLEncoding.DecodeString(cookie)
if err != nil {
authboss.DelCookie(w, authboss.CookieRemember)
logger.Infof("failed to decode remember me cookie, deleting cookie")
return nil
}
index := bytes.IndexByte(rawToken, ';')
if index < 0 {
authboss.DelCookie(w, authboss.CookieRemember)
logger.Infof("failed to decode remember me token, deleting cookie")
return nil
}
pid := string(rawToken[:index])
sum := sha512.Sum512(rawToken)
hash := base64.StdEncoding.EncodeToString(sum[:])
storer := authboss.EnsureCanRemember(ab.Config.Storage.Server)
err = storer.UseRememberToken((*req).Context(), pid, hash)
switch {
case err == authboss.ErrTokenNotFound:
logger.Infof("remember me cookie had a token that was not in storage, deleting cookie")
authboss.DelCookie(w, authboss.CookieRemember)
return nil
case err != nil:
return err
}
hash, token, err := GenerateToken(pid)
if err != nil {
return err
}
if err = storer.AddRememberToken((*req).Context(), pid, hash); err != nil {
return errors.Wrap(err, "failed to save remember me token")
}
*req = (*req).WithContext(context.WithValue((*req).Context(), authboss.CTXKeyPID, pid))
authboss.PutSession(w, authboss.SessionKey, pid)
authboss.PutSession(w, authboss.SessionHalfAuthKey, "true")
authboss.DelCookie(w, authboss.CookieRemember)
authboss.PutCookie(w, authboss.CookieRemember, token)
return nil
}
// AfterPasswordReset is called after the password has been reset, since
// it should invalidate all tokens associated to that user.
func (r *Remember) AfterPasswordReset(w http.ResponseWriter, req *http.Request, handled bool) (bool, error) {
user, err := r.Authboss.CurrentUser(req)
if err != nil {
return false, err
}
logger := r.Authboss.RequestLogger(req)
storer := authboss.EnsureCanRemember(r.Authboss.Config.Storage.Server)
pid := user.GetPID()
authboss.DelCookie(w, authboss.CookieRemember)
logger.Infof("deleting tokens and rm cookies for user %s due to password reset", pid)
return false, storer.DelRememberTokens(req.Context(), pid)
}
// GenerateToken creates a remember me token
func GenerateToken(pid string) (hash string, token string, err error) {
rawToken := make([]byte, nNonceSize+len(pid)+1)
copy(rawToken, pid)
rawToken[len(pid)] = ';'
if _, err := io.ReadFull(rand.Reader, rawToken[len(pid)+1:]); err != nil {
return "", "", errors.Wrap(err, "failed to create remember me nonce")
}
sum := sha512.Sum512(rawToken)
return base64.StdEncoding.EncodeToString(sum[:]), base64.URLEncoding.EncodeToString(rawToken), nil
}