2015-01-11 08:52:39 +02:00
|
|
|
// Package remember implements persistent logins through (typically) cookie session
|
|
|
|
// storages. The SessionStorer implementation must be fully secure either over https
|
|
|
|
// or using signed cookies or it is easily exploitable.
|
|
|
|
package remember
|
|
|
|
|
|
|
|
import (
|
2015-01-13 00:02:07 +02:00
|
|
|
"bytes"
|
2015-01-11 08:52:39 +02:00
|
|
|
"crypto/md5"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
2015-01-13 00:02:07 +02:00
|
|
|
"fmt"
|
2015-01-11 08:52:39 +02:00
|
|
|
|
|
|
|
"gopkg.in/authboss.v0"
|
|
|
|
)
|
|
|
|
|
2015-01-13 00:02:07 +02:00
|
|
|
const (
|
2015-02-22 22:55:09 +02:00
|
|
|
// RememberKey is used for cookies and form input names.
|
|
|
|
RememberKey = "rm"
|
2015-02-24 21:04:27 +02:00
|
|
|
nRandBytes = 32
|
2015-02-22 22:55:09 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
errUserMissing = errors.New("remember: User not loaded in callback")
|
2015-01-13 00:02:07 +02:00
|
|
|
)
|
|
|
|
|
2015-02-24 21:04:27 +02:00
|
|
|
// TokenStorer 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.
|
|
|
|
type TokenStorer interface {
|
|
|
|
authboss.Storer
|
|
|
|
// 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 the key that was found. If the token could not be found
|
|
|
|
// return "", ErrTokenNotFound
|
|
|
|
UseToken(givenKey, token string) (key string, err error)
|
|
|
|
}
|
2015-01-11 08:52:39 +02:00
|
|
|
|
|
|
|
func init() {
|
2015-02-27 09:09:37 +02:00
|
|
|
authboss.RegisterModule("remember", &Remember{})
|
2015-01-11 08:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-02-16 06:07:36 +02:00
|
|
|
type Remember struct{}
|
2015-01-11 08:52:39 +02:00
|
|
|
|
2015-02-16 06:07:36 +02:00
|
|
|
func (r *Remember) Initialize() error {
|
|
|
|
if authboss.Cfg.Storer == nil {
|
2015-02-22 22:55:09 +02:00
|
|
|
return errors.New("remember: Need a TokenStorer")
|
2015-01-13 00:02:07 +02:00
|
|
|
}
|
|
|
|
|
2015-02-24 21:04:27 +02:00
|
|
|
if _, ok := authboss.Cfg.Storer.(TokenStorer); !ok {
|
2015-02-22 22:55:09 +02:00
|
|
|
return errors.New("remember: TokenStorer required for remember me functionality")
|
2015-01-11 08:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-03-02 06:40:09 +02:00
|
|
|
authboss.Cfg.Callbacks.Before(authboss.EventGetUserSession, r.auth)
|
2015-02-27 09:09:37 +02:00
|
|
|
authboss.Cfg.Callbacks.After(authboss.EventAuth, r.afterAuth)
|
2015-01-13 00:02:07 +02:00
|
|
|
|
2015-01-11 08:52:39 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Remember) Routes() authboss.RouteTable {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Remember) Storage() authboss.StorageOptions {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-02-27 09:09:37 +02:00
|
|
|
// afterAuth is called after authentication is successful.
|
|
|
|
func (r *Remember) afterAuth(ctx *authboss.Context) error {
|
2015-02-22 22:55:09 +02:00
|
|
|
if val, ok := ctx.FirstPostFormValue(RememberKey); !ok || val != "true" {
|
|
|
|
return nil
|
2015-01-13 00:02:07 +02:00
|
|
|
}
|
|
|
|
|
2015-01-15 12:56:13 +02:00
|
|
|
if ctx.User == nil {
|
2015-02-22 22:55:09 +02:00
|
|
|
return errUserMissing
|
2015-01-15 23:24:12 +02:00
|
|
|
}
|
2015-01-16 01:10:47 +02:00
|
|
|
|
2015-02-22 23:16:11 +02:00
|
|
|
key, err := ctx.User.StringErr(authboss.Cfg.PrimaryID)
|
2015-02-22 22:55:09 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2015-01-15 05:18:45 +02:00
|
|
|
}
|
2015-01-13 00:02:07 +02:00
|
|
|
|
2015-02-27 09:09:37 +02:00
|
|
|
if _, err := r.new(ctx.CookieStorer, key); err != nil {
|
2015-02-22 22:55:09 +02:00
|
|
|
return fmt.Errorf("remember: Failed to create remember token: %v", err)
|
2015-01-13 00:02:07 +02:00
|
|
|
}
|
2015-02-22 22:55:09 +02:00
|
|
|
|
|
|
|
return nil
|
2015-01-13 00:02:07 +02:00
|
|
|
}
|
|
|
|
|
2015-02-27 09:09:37 +02:00
|
|
|
// new generates a new remember token and stores it in the configured TokenStorer.
|
2015-01-11 08:52:39 +02:00
|
|
|
// 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.
|
2015-02-27 09:09:37 +02:00
|
|
|
func (r *Remember) new(cstorer authboss.ClientStorer, storageKey string) (string, error) {
|
2015-01-13 00:02:07 +02:00
|
|
|
token := make([]byte, nRandBytes+len(storageKey)+1)
|
|
|
|
copy(token, []byte(storageKey))
|
|
|
|
token[len(storageKey)] = ';'
|
2015-01-11 08:52:39 +02:00
|
|
|
|
2015-01-13 00:02:07 +02:00
|
|
|
if _, err := rand.Read(token[len(storageKey)+1:]); err != nil {
|
|
|
|
return "", err
|
2015-01-11 08:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sum := md5.Sum(token)
|
|
|
|
finalToken := base64.URLEncoding.EncodeToString(token)
|
|
|
|
storageToken := base64.StdEncoding.EncodeToString(sum[:])
|
|
|
|
|
2015-01-13 00:02:07 +02:00
|
|
|
// Save the token in the DB
|
2015-02-24 21:04:27 +02:00
|
|
|
if err := authboss.Cfg.Storer.(TokenStorer).AddToken(storageKey, storageToken); err != nil {
|
2015-01-11 08:52:39 +02:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2015-01-13 00:02:07 +02:00
|
|
|
// Write the finalToken to the cookie
|
2015-02-22 22:55:09 +02:00
|
|
|
cstorer.Put(RememberKey, finalToken)
|
2015-01-13 00:02:07 +02:00
|
|
|
|
2015-01-11 08:52:39 +02:00
|
|
|
return finalToken, nil
|
|
|
|
}
|
|
|
|
|
2015-02-27 09:09:37 +02:00
|
|
|
// auth takes a token that was given to a user and checks to see if something
|
2015-01-11 08:52:39 +02:00
|
|
|
// is matching in the database. If something is found the old token is deleted
|
2015-03-02 06:40:09 +02:00
|
|
|
// and a new one should be generated.
|
2015-02-27 09:09:37 +02:00
|
|
|
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(RememberKey)
|
|
|
|
if !ok {
|
|
|
|
return authboss.InterruptNone, nil
|
|
|
|
}
|
|
|
|
|
2015-01-11 08:52:39 +02:00
|
|
|
token, err := base64.URLEncoding.DecodeString(finalToken)
|
|
|
|
if err != nil {
|
2015-02-27 09:09:37 +02:00
|
|
|
return authboss.InterruptNone, err
|
2015-01-11 08:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-01-13 00:02:07 +02:00
|
|
|
index := bytes.IndexByte(token, ';')
|
|
|
|
if index < 0 {
|
2015-02-27 09:09:37 +02:00
|
|
|
return authboss.InterruptNone, errors.New("remember: Invalid remember me token.")
|
2015-01-13 00:02:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get the key.
|
|
|
|
givenKey := token[:index]
|
|
|
|
|
|
|
|
// Verify the tokens match.
|
2015-01-11 08:52:39 +02:00
|
|
|
sum := md5.Sum(token)
|
2015-01-13 00:02:07 +02:00
|
|
|
|
2015-02-24 21:04:27 +02:00
|
|
|
key, err := authboss.Cfg.Storer.(TokenStorer).UseToken(string(givenKey), base64.StdEncoding.EncodeToString(sum[:]))
|
2015-01-24 01:56:24 +02:00
|
|
|
if err == authboss.ErrTokenNotFound {
|
2015-02-27 09:09:37 +02:00
|
|
|
return authboss.InterruptNone, nil
|
2015-01-11 08:52:39 +02:00
|
|
|
} else if err != nil {
|
2015-02-27 09:09:37 +02:00
|
|
|
return authboss.InterruptNone, err
|
2015-01-11 08:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-03-02 06:40:09 +02:00
|
|
|
_, err = r.new(ctx.CookieStorer, string(key))
|
|
|
|
if err != nil {
|
|
|
|
return authboss.InterruptNone, err
|
|
|
|
}
|
|
|
|
|
2015-01-13 00:02:07 +02:00
|
|
|
// Ensure a half-auth.
|
2015-02-27 09:09:37 +02:00
|
|
|
ctx.SessionStorer.Put(authboss.SessionHalfAuthKey, "true")
|
2015-01-13 00:02:07 +02:00
|
|
|
// Log the user in.
|
2015-03-02 06:40:09 +02:00
|
|
|
ctx.SessionStorer.Put(authboss.SessionKey, string(key))
|
2015-01-13 00:02:07 +02:00
|
|
|
|
2015-02-27 09:09:37 +02:00
|
|
|
return authboss.InterruptNone, nil
|
2015-01-11 08:52:39 +02:00
|
|
|
}
|