2015-02-07 04:27:12 -08:00
|
|
|
// Package confirm implements user confirming after N bad sign-in attempts.
|
|
|
|
package confirm
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/md5"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2015-02-10 00:43:45 -08:00
|
|
|
"net/url"
|
2015-02-07 04:27:12 -08:00
|
|
|
|
|
|
|
"gopkg.in/authboss.v0"
|
|
|
|
"gopkg.in/authboss.v0/internal/views"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
UserConfirmToken = "confirm_token"
|
|
|
|
UserConfirmed = "confirmed"
|
2015-02-10 00:43:45 -08:00
|
|
|
UserEmail = "email"
|
2015-02-07 04:27:12 -08:00
|
|
|
|
|
|
|
FormValueConfirm = "cnf"
|
|
|
|
|
|
|
|
tplConfirmHTML = "confirm_email.html.tpl"
|
2015-02-10 16:29:52 -08:00
|
|
|
tplConfirmText = "confirm_email.txt.tpl"
|
2015-02-07 04:27:12 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// ErrNotConfirmed happens when the account is there, but
|
|
|
|
// not yet confirmed.
|
|
|
|
ErrNotConfirmed = errors.New("Account is not confirmed.")
|
|
|
|
)
|
|
|
|
|
|
|
|
// C is the singleton instance of the confirm module which will have been
|
|
|
|
// configured and ready to use after authboss.Init()
|
|
|
|
var C *Confirm
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
C = &Confirm{}
|
|
|
|
authboss.RegisterModule("confirm", C)
|
|
|
|
}
|
|
|
|
|
|
|
|
type Confirm struct {
|
|
|
|
logger io.Writer
|
2015-02-10 00:43:45 -08:00
|
|
|
storer authboss.ConfirmStorer
|
2015-02-07 04:27:12 -08:00
|
|
|
|
|
|
|
config *authboss.Config
|
|
|
|
emailTemplates views.Templates
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Confirm) Initialize(config *authboss.Config) (err error) {
|
2015-02-10 00:43:45 -08:00
|
|
|
var ok bool
|
|
|
|
c.storer, ok = config.Storer.(authboss.ConfirmStorer)
|
|
|
|
if config.Storer == nil || !ok {
|
|
|
|
return errors.New("confirm: Need a ConfirmStorer.")
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
c.logger = config.LogWriter
|
|
|
|
c.config = config
|
|
|
|
|
|
|
|
c.emailTemplates, err = views.Get(config.LayoutEmail, config.ViewsPath, tplConfirmHTML, tplConfirmText)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
config.Callbacks.Before(authboss.EventGet, c.BeforeGet)
|
|
|
|
config.Callbacks.After(authboss.EventRegister, c.AfterRegister)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Confirm) Routes() authboss.RouteTable {
|
|
|
|
return authboss.RouteTable{
|
|
|
|
"/confirm": c.confirmHandler,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Confirm) Storage() authboss.StorageOptions {
|
|
|
|
return authboss.StorageOptions{
|
|
|
|
UserConfirmToken: authboss.String,
|
|
|
|
UserConfirmed: authboss.Bool,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Confirm) BeforeGet(ctx *authboss.Context) error {
|
|
|
|
if intf, ok := ctx.User[UserConfirmed]; ok {
|
2015-02-10 00:43:45 -08:00
|
|
|
if confirmed, ok := intf.(bool); !ok && !confirmed {
|
2015-02-07 04:27:12 -08:00
|
|
|
return ErrNotConfirmed
|
|
|
|
}
|
|
|
|
}
|
2015-02-10 00:43:45 -08:00
|
|
|
|
|
|
|
return nil
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// AfterRegister ensures the account is not activated.
|
|
|
|
func (c *Confirm) AfterRegister(ctx *authboss.Context) {
|
|
|
|
if ctx.User == nil {
|
2015-02-10 00:43:45 -08:00
|
|
|
fmt.Fprintln(c.logger, "confirm: user not loaded in AfterRegister callback")
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
token := make([]byte, 32)
|
|
|
|
if _, err := rand.Read(token); err != nil {
|
|
|
|
fmt.Fprintln(c.logger, "confirm: failed to produce random token:", err)
|
|
|
|
}
|
|
|
|
sum := md5.Sum(token)
|
|
|
|
|
|
|
|
ctx.User[UserConfirmToken] = base64.StdEncoding.EncodeToString(sum[:])
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
username, _ := ctx.User.String(authboss.UserName)
|
|
|
|
|
2015-02-07 04:27:12 -08:00
|
|
|
if err := ctx.SaveUser(username, c.config.Storer); err != nil {
|
2015-02-10 00:43:45 -08:00
|
|
|
fmt.Fprintln(c.logger, "confirm: failed to save user's token:", err)
|
|
|
|
return
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
if email, ok := ctx.User.String(UserEmail); !ok {
|
2015-02-07 04:27:12 -08:00
|
|
|
fmt.Fprintln(c.logger, "confirm: user has no e-mail address to send to, could not send confirm e-mail")
|
|
|
|
} else {
|
2015-02-10 00:43:45 -08:00
|
|
|
goConfirmEmail(c, email, base64.URLEncoding.EncodeToString(sum[:]))
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
var goConfirmEmail = func(c *Confirm, to, token string) {
|
|
|
|
go c.confirmEmail(to, token)
|
|
|
|
}
|
|
|
|
|
2015-02-07 04:27:12 -08:00
|
|
|
// confirmEmail sends a confirmation e-mail.
|
|
|
|
func (c *Confirm) confirmEmail(to, token string) {
|
2015-02-10 00:43:45 -08:00
|
|
|
url := fmt.Sprintf("%s/confirm?%s=%s", c.config.HostName, url.QueryEscape(FormValueConfirm), url.QueryEscape(token))
|
2015-02-07 04:27:12 -08:00
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
var htmlEmailBody, textEmailBody *bytes.Buffer
|
|
|
|
var err error
|
|
|
|
if htmlEmailBody, err = c.emailTemplates.ExecuteTemplate(tplConfirmHTML, url); err != nil {
|
2015-02-07 04:27:12 -08:00
|
|
|
fmt.Fprintln(c.logger, "confirm: failed to build html template:", err)
|
2015-02-10 00:43:45 -08:00
|
|
|
return
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
if textEmailBody, err = c.emailTemplates.ExecuteTemplate(tplConfirmText, url); err != nil {
|
2015-02-07 04:27:12 -08:00
|
|
|
fmt.Fprintln(c.logger, "confirm: failed to build plaintext template:", err)
|
2015-02-10 00:43:45 -08:00
|
|
|
return
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
if err := c.config.Mailer.Send(authboss.Email{
|
2015-02-07 04:27:12 -08:00
|
|
|
To: []string{to},
|
|
|
|
From: c.config.EmailFrom,
|
|
|
|
Subject: c.config.EmailSubjectPrefix + "Confirm New Account",
|
|
|
|
TextBody: textEmailBody.String(),
|
|
|
|
HTMLBody: htmlEmailBody.String(),
|
|
|
|
}); err != nil {
|
|
|
|
fmt.Fprintln(c.logger, "confirm: failed to build plaintext template:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
func (c *Confirm) confirmHandler(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
|
|
|
|
token, ok := ctx.FirstFormValue(FormValueConfirm)
|
|
|
|
if len(token) == 0 || !ok {
|
|
|
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
|
|
fmt.Fprintln(c.logger, "confirm: no confirm token found in get")
|
|
|
|
return
|
|
|
|
}
|
2015-02-07 04:27:12 -08:00
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
tok, err := base64.URLEncoding.DecodeString(token)
|
2015-02-07 04:27:12 -08:00
|
|
|
if err != nil {
|
2015-02-10 00:43:45 -08:00
|
|
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
|
|
fmt.Fprintf(c.logger, "confirm: confirm token failed to decode %q => %v\n", token, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dbTok := base64.StdEncoding.EncodeToString(tok)
|
|
|
|
user, err := c.storer.ConfirmUser(dbTok)
|
|
|
|
if err == authboss.ErrUserNotFound {
|
|
|
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
|
|
fmt.Fprintln(c.logger, "confirm: token not found", err)
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
|
|
|
w.WriteHeader(500)
|
|
|
|
fmt.Fprintln(c.logger, "confirm: error retrieving user token:", err)
|
|
|
|
return
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
ctx.User = authboss.Unbind(user)
|
2015-02-07 04:27:12 -08:00
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
ctx.User[UserConfirmToken] = ""
|
|
|
|
ctx.User[UserConfirmed] = true
|
|
|
|
|
|
|
|
key, ok := ctx.User.String(authboss.UserName)
|
2015-02-07 04:27:12 -08:00
|
|
|
if !ok {
|
2015-02-10 00:43:45 -08:00
|
|
|
w.WriteHeader(500)
|
|
|
|
fmt.Fprintln(c.logger, "confirm: user had no key field")
|
|
|
|
return
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
ctx.SessionStorer.Put(authboss.SessionKey, key)
|
|
|
|
ctx.SessionStorer.Put(authboss.FlashSuccessKey, "Successfully confirmed your account.")
|
2015-02-07 04:27:12 -08:00
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
if err := ctx.SaveUser(key, c.config.Storer); err != nil {
|
|
|
|
fmt.Fprintln(c.logger, "confirm: failed to clear the user's token:", err)
|
|
|
|
return
|
|
|
|
}
|
2015-02-07 04:27:12 -08:00
|
|
|
|
2015-02-10 00:43:45 -08:00
|
|
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
2015-02-07 04:27:12 -08:00
|
|
|
}
|