1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-01-08 04:03:53 +02:00
authboss/remember/remember.go
Aaron L be041cbae6 remember: Context+Request separation ripple
- Re-add the age-old "Values" from the Context. This was originally
  there for exactly the documented purpose. However the Context holding
  the request form values negated it's use. It's back because of this
  new separation.
- Make the auth success path set the authboss.CookieRemember value in
  the context before calling it's callback.
2015-08-02 14:02:14 -07:00

256 lines
6.4 KiB
Go

// Package remember implements persistent logins through the cookie storer.
package remember
import (
"bytes"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"gopkg.in/authboss.v0"
)
const (
nRandBytes = 32
)
var (
errUserMissing = errors.New("remember: User not loaded in callback")
)
// RememberStorer must be implemented in order to satisfy the remember module's
// storage requirements. If the implementer is a typical database then
// the tokens should be stored in a separate table since they require a 1-n
// with the user for each device the user wishes to remain logged in on.
//
// Remember storer will look at both authboss's configured Storer and OAuth2Storer
// for compatibility.
type RememberStorer interface {
// AddToken saves a new token for the key.
AddToken(key, token string) error
// DelTokens removes all tokens for a given key.
DelTokens(key string) error
// UseToken finds the key-token pair, removes the entry in the store
// and returns nil. If the token could not be found return ErrTokenNotFound.
UseToken(givenKey, token string) (err error)
}
func init() {
authboss.RegisterModule("remember", &Remember{})
}
// Remember module
type Remember struct {
*authboss.Authboss
}
// Initialize module
func (r *Remember) Initialize(ab *authboss.Authboss) error {
r.Authboss = ab
if r.Storer == nil && r.OAuth2Storer == nil {
return errors.New("remember: Need a RememberStorer")
}
if _, ok := r.Storer.(RememberStorer); !ok {
if _, ok := r.OAuth2Storer.(RememberStorer); !ok {
return errors.New("remember: RememberStorer required for remember functionality")
}
}
r.Callbacks.Before(authboss.EventGetUserSession, r.auth)
r.Callbacks.After(authboss.EventAuth, r.afterAuth)
r.Callbacks.After(authboss.EventOAuth, r.afterOAuth)
r.Callbacks.After(authboss.EventPasswordReset, r.afterPassword)
return nil
}
// Routes for module
func (r *Remember) Routes() authboss.RouteTable {
return nil
}
// Storage requirements
func (r *Remember) Storage() authboss.StorageOptions {
return authboss.StorageOptions{
r.PrimaryID: authboss.String,
}
}
// afterAuth is called after authentication is successful.
func (r *Remember) afterAuth(ctx *authboss.Context) error {
if val := ctx.Values[authboss.CookieRemember]; val != "true" {
return nil
}
if ctx.User == nil {
return errUserMissing
}
key, err := ctx.User.StringErr(r.PrimaryID)
if err != nil {
return err
}
if _, err := r.new(ctx.CookieStorer, key); err != nil {
return fmt.Errorf("remember: Failed to create remember token: %v", err)
}
return nil
}
// afterOAuth is called after oauth authentication is successful.
// Has to pander to horrible state variable packing to figure out if we want
// to be remembered.
func (r *Remember) afterOAuth(ctx *authboss.Context) error {
sessValues, ok := ctx.SessionStorer.Get(authboss.SessionOAuth2Params)
if !ok {
return nil
}
var values map[string]string
if err := json.Unmarshal([]byte(sessValues), &values); err != nil {
return err
}
val, ok := values[authboss.CookieRemember]
should := ok && val == "true"
if !should {
return nil
}
if ctx.User == nil {
return errUserMissing
}
uid, err := ctx.User.StringErr(authboss.StoreOAuth2Provider)
if err != nil {
return err
}
provider, err := ctx.User.StringErr(authboss.StoreOAuth2Provider)
if err != nil {
return err
}
if _, err := r.new(ctx.CookieStorer, uid+";"+provider); err != nil {
return fmt.Errorf("remember: Failed to create remember token: %v", err)
}
return nil
}
// afterPassword is called after the password has been reset.
func (r *Remember) afterPassword(ctx *authboss.Context) error {
if ctx.User == nil {
return nil
}
id, ok := ctx.User.String(r.PrimaryID)
if !ok {
return nil
}
ctx.CookieStorer.Del(authboss.CookieRemember)
var storer RememberStorer
if storer, ok = r.Storer.(RememberStorer); !ok {
if storer, ok = r.OAuth2Storer.(RememberStorer); !ok {
return nil
}
}
return storer.DelTokens(id)
}
// new generates a new remember token and stores it in the configured RememberStorer.
// 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(cstorer authboss.ClientStorer, storageKey string) (string, error) {
token := make([]byte, nRandBytes+len(storageKey)+1)
copy(token, []byte(storageKey))
token[len(storageKey)] = ';'
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[:])
var storer RememberStorer
var ok bool
if storer, ok = r.Storer.(RememberStorer); !ok {
storer, ok = r.OAuth2Storer.(RememberStorer)
}
// Save the token in the DB
if err := storer.AddToken(storageKey, storageToken); err != nil {
return "", err
}
// Write the finalToken to the cookie
cstorer.Put(authboss.CookieRemember, finalToken)
return finalToken, nil
}
// auth takes a token that was given to a user and checks to see if something
// is matching in the database. If something is found the old token is deleted
// and a new one should be generated.
func (r *Remember) auth(ctx *authboss.Context) (authboss.Interrupt, error) {
if val, ok := ctx.SessionStorer.Get(authboss.SessionKey); ok || len(val) > 0 {
return authboss.InterruptNone, nil
}
finalToken, ok := ctx.CookieStorer.Get(authboss.CookieRemember)
if !ok {
return authboss.InterruptNone, nil
}
token, err := base64.URLEncoding.DecodeString(finalToken)
if err != nil {
return authboss.InterruptNone, err
}
index := bytes.IndexByte(token, ';')
if index < 0 {
return authboss.InterruptNone, errors.New("remember: Invalid remember token")
}
// Get the key.
givenKey := string(token[:index])
// Verify the tokens match.
sum := md5.Sum(token)
var storer RememberStorer
if storer, ok = r.Storer.(RememberStorer); !ok {
storer, ok = r.OAuth2Storer.(RememberStorer)
}
err = storer.UseToken(givenKey, base64.StdEncoding.EncodeToString(sum[:]))
if err == authboss.ErrTokenNotFound {
return authboss.InterruptNone, nil
} else if err != nil {
return authboss.InterruptNone, err
}
_, err = r.new(ctx.CookieStorer, givenKey)
if err != nil {
return authboss.InterruptNone, err
}
// Ensure a half-auth.
ctx.SessionStorer.Put(authboss.SessionHalfAuthKey, "true")
// Log the user in.
ctx.SessionStorer.Put(authboss.SessionKey, givenKey)
return authboss.InterruptNone, nil
}