github.com/volatiletech/authboss@v2.4.1+incompatible/confirm/confirm.go (about)

     1  // Package confirm implements confirmation of user registration via e-mail
     2  package confirm
     3  
     4  import (
     5  	"context"
     6  	"crypto/rand"
     7  	"crypto/sha512"
     8  	"crypto/subtle"
     9  	"encoding/base64"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"net/url"
    14  	"path"
    15  
    16  	"github.com/pkg/errors"
    17  
    18  	"github.com/volatiletech/authboss"
    19  )
    20  
    21  const (
    22  	// PageConfirm is only really used for the BodyReader
    23  	PageConfirm = "confirm"
    24  
    25  	// EmailConfirmHTML is the name of the html template for e-mails
    26  	EmailConfirmHTML = "confirm_html"
    27  	// EmailConfirmTxt is the name of the text template for e-mails
    28  	EmailConfirmTxt = "confirm_txt"
    29  
    30  	// FormValueConfirm is the name of the form value for
    31  	FormValueConfirm = "cnf"
    32  
    33  	// DataConfirmURL is the name of the e-mail template variable
    34  	// that gives the url to send to the user for confirmation.
    35  	DataConfirmURL = "url"
    36  
    37  	confirmTokenSize  = 64
    38  	confirmTokenSplit = confirmTokenSize / 2
    39  )
    40  
    41  func init() {
    42  	authboss.RegisterModule("confirm", &Confirm{})
    43  }
    44  
    45  // Confirm module
    46  type Confirm struct {
    47  	*authboss.Authboss
    48  }
    49  
    50  // Init module
    51  func (c *Confirm) Init(ab *authboss.Authboss) (err error) {
    52  	c.Authboss = ab
    53  
    54  	if err = c.Authboss.Config.Core.MailRenderer.Load(EmailConfirmHTML, EmailConfirmTxt); err != nil {
    55  		return err
    56  	}
    57  
    58  	var callbackMethod func(string, http.Handler)
    59  	methodConfig := c.Config.Modules.ConfirmMethod
    60  	if methodConfig == http.MethodGet {
    61  		methodConfig = c.Config.Modules.MailRouteMethod
    62  	}
    63  	switch methodConfig {
    64  	case http.MethodGet:
    65  		callbackMethod = c.Authboss.Config.Core.Router.Get
    66  	case http.MethodPost:
    67  		callbackMethod = c.Authboss.Config.Core.Router.Post
    68  	default:
    69  		panic("invalid config for ConfirmMethod/MailRouteMethod")
    70  	}
    71  	callbackMethod("/confirm", c.Authboss.Config.Core.ErrorHandler.Wrap(c.Get))
    72  
    73  	c.Events.Before(authboss.EventAuth, c.PreventAuth)
    74  	c.Events.After(authboss.EventRegister, c.StartConfirmationWeb)
    75  
    76  	return nil
    77  }
    78  
    79  // PreventAuth stops the EventAuth from succeeding when a user is not confirmed
    80  // This relies on the fact that the context holds the user at this point in time
    81  // loaded by the auth module (or something else).
    82  func (c *Confirm) PreventAuth(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
    83  	logger := c.Authboss.RequestLogger(r)
    84  
    85  	user, err := c.Authboss.CurrentUser(r)
    86  	if err != nil {
    87  		return false, err
    88  	}
    89  
    90  	cuser := authboss.MustBeConfirmable(user)
    91  	if cuser.GetConfirmed() {
    92  		logger.Infof("user %s is confirmed, allowing auth", user.GetPID())
    93  		return false, nil
    94  	}
    95  
    96  	logger.Infof("user %s was not confirmed, preventing auth", user.GetPID())
    97  	ro := authboss.RedirectOptions{
    98  		Code:         http.StatusTemporaryRedirect,
    99  		RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
   100  		Failure:      "Your account has not been confirmed, please check your e-mail.",
   101  	}
   102  	return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
   103  }
   104  
   105  // StartConfirmationWeb hijacks a request and forces a user to be confirmed
   106  // first it's assumed that the current user is loaded into the request context.
   107  func (c *Confirm) StartConfirmationWeb(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   108  	user, err := c.Authboss.CurrentUser(r)
   109  	if err != nil {
   110  		return false, err
   111  	}
   112  
   113  	cuser := authboss.MustBeConfirmable(user)
   114  	if err = c.StartConfirmation(r.Context(), cuser, true); err != nil {
   115  		return false, err
   116  	}
   117  
   118  	ro := authboss.RedirectOptions{
   119  		Code:         http.StatusTemporaryRedirect,
   120  		RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
   121  		Success:      "Please verify your account, an e-mail has been sent to you.",
   122  	}
   123  	return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
   124  }
   125  
   126  // StartConfirmation begins confirmation on a user by setting them to require
   127  // confirmation via a created token, and optionally sending them an e-mail.
   128  func (c *Confirm) StartConfirmation(ctx context.Context, user authboss.ConfirmableUser, sendEmail bool) error {
   129  	logger := c.Authboss.Logger(ctx)
   130  
   131  	selector, verifier, token, err := GenerateConfirmCreds()
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	user.PutConfirmed(false)
   137  	user.PutConfirmSelector(selector)
   138  	user.PutConfirmVerifier(verifier)
   139  
   140  	logger.Infof("generated new confirm token for user: %s", user.GetPID())
   141  	if err := c.Authboss.Config.Storage.Server.Save(ctx, user); err != nil {
   142  		return errors.Wrap(err, "failed to save user during StartConfirmation, user data may be in weird state")
   143  	}
   144  
   145  	if c.Authboss.Config.Modules.MailNoGoroutine {
   146  		c.SendConfirmEmail(ctx, user.GetEmail(), token)
   147  	} else {
   148  		go c.SendConfirmEmail(ctx, user.GetEmail(), token)
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  // SendConfirmEmail sends a confirmation e-mail to a user
   155  func (c *Confirm) SendConfirmEmail(ctx context.Context, to, token string) {
   156  	logger := c.Authboss.Logger(ctx)
   157  
   158  	mailURL := c.mailURL(token)
   159  
   160  	email := authboss.Email{
   161  		To:       []string{to},
   162  		From:     c.Config.Mail.From,
   163  		FromName: c.Config.Mail.FromName,
   164  		Subject:  c.Config.Mail.SubjectPrefix + "Confirm New Account",
   165  	}
   166  
   167  	logger.Infof("sending confirm e-mail to: %s", to)
   168  
   169  	ro := authboss.EmailResponseOptions{
   170  		Data:         authboss.NewHTMLData(DataConfirmURL, mailURL),
   171  		HTMLTemplate: EmailConfirmHTML,
   172  		TextTemplate: EmailConfirmTxt,
   173  	}
   174  	if err := c.Authboss.Email(ctx, email, ro); err != nil {
   175  		logger.Errorf("failed to send confirm e-mail to %s: %+v", to, err)
   176  	}
   177  }
   178  
   179  // Get is a request that confirms a user with a valid token
   180  func (c *Confirm) Get(w http.ResponseWriter, r *http.Request) error {
   181  	logger := c.RequestLogger(r)
   182  
   183  	validator, err := c.Authboss.Config.Core.BodyReader.Read(PageConfirm, r)
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	if errs := validator.Validate(); errs != nil {
   189  		logger.Infof("validation failed in Confirm.Get, this typically means a bad token: %+v", errs)
   190  		return c.invalidToken(w, r)
   191  	}
   192  
   193  	values := authboss.MustHaveConfirmValues(validator)
   194  
   195  	rawToken, err := base64.URLEncoding.DecodeString(values.GetToken())
   196  	if err != nil {
   197  		logger.Infof("error decoding token in Confirm.Get, this typically means a bad token: %s %+v", values.GetToken(), err)
   198  		return c.invalidToken(w, r)
   199  	}
   200  
   201  	if len(rawToken) != confirmTokenSize {
   202  		logger.Infof("invalid confirm token submitted, size was wrong: %d", len(rawToken))
   203  		return c.invalidToken(w, r)
   204  	}
   205  
   206  	selectorBytes := sha512.Sum512(rawToken[:confirmTokenSplit])
   207  	verifierBytes := sha512.Sum512(rawToken[confirmTokenSplit:])
   208  	selector := base64.StdEncoding.EncodeToString(selectorBytes[:])
   209  
   210  	storer := authboss.EnsureCanConfirm(c.Authboss.Config.Storage.Server)
   211  	user, err := storer.LoadByConfirmSelector(r.Context(), selector)
   212  	if err == authboss.ErrUserNotFound {
   213  		logger.Infof("confirm selector was not found in database: %s", selector)
   214  		return c.invalidToken(w, r)
   215  	} else if err != nil {
   216  		return err
   217  	}
   218  
   219  	dbVerifierBytes, err := base64.StdEncoding.DecodeString(user.GetConfirmVerifier())
   220  	if err != nil {
   221  		logger.Infof("invalid confirm verifier stored in database: %s", user.GetConfirmVerifier())
   222  		return c.invalidToken(w, r)
   223  	}
   224  
   225  	if subtle.ConstantTimeEq(int32(len(verifierBytes)), int32(len(dbVerifierBytes))) != 1 ||
   226  		subtle.ConstantTimeCompare(verifierBytes[:], dbVerifierBytes) != 1 {
   227  		logger.Info("stored confirm verifier does not match provided one")
   228  		return c.invalidToken(w, r)
   229  	}
   230  
   231  	user.PutConfirmSelector("")
   232  	user.PutConfirmVerifier("")
   233  	user.PutConfirmed(true)
   234  
   235  	logger.Infof("user %s confirmed their account", user.GetPID())
   236  	if err = c.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil {
   237  		return err
   238  	}
   239  
   240  	ro := authboss.RedirectOptions{
   241  		Code:         http.StatusTemporaryRedirect,
   242  		Success:      "You have successfully confirmed your account.",
   243  		RedirectPath: c.Authboss.Config.Paths.ConfirmOK,
   244  	}
   245  	return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
   246  }
   247  
   248  func (c *Confirm) mailURL(token string) string {
   249  	query := url.Values{FormValueConfirm: []string{token}}
   250  
   251  	if len(c.Config.Mail.RootURL) != 0 {
   252  		return fmt.Sprintf("%s?%s", c.Config.Mail.RootURL+"/confirm", query.Encode())
   253  	}
   254  
   255  	p := path.Join(c.Config.Paths.Mount, "confirm")
   256  	return fmt.Sprintf("%s%s?%s", c.Config.Paths.RootURL, p, query.Encode())
   257  }
   258  
   259  func (c *Confirm) invalidToken(w http.ResponseWriter, r *http.Request) error {
   260  	ro := authboss.RedirectOptions{
   261  		Code:         http.StatusTemporaryRedirect,
   262  		Failure:      "confirm token is invalid",
   263  		RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
   264  	}
   265  	return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
   266  }
   267  
   268  // Middleware ensures that a user is confirmed, or else it will intercept the
   269  // request and send them to the confirm page, this will load the user if he's
   270  // not been loaded yet from the session.
   271  //
   272  // Panics if the user was not able to be loaded in order to allow a panic
   273  // handler to show a nice error page, also panics if it failed to redirect
   274  // for whatever reason.
   275  func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
   276  	return func(next http.Handler) http.Handler {
   277  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   278  			user := ab.LoadCurrentUserP(&r)
   279  
   280  			cu := authboss.MustBeConfirmable(user)
   281  			if cu.GetConfirmed() {
   282  				next.ServeHTTP(w, r)
   283  				return
   284  			}
   285  
   286  			logger := ab.RequestLogger(r)
   287  			logger.Infof("user %s prevented from accessing %s: not confirmed", user.GetPID(), r.URL.Path)
   288  			ro := authboss.RedirectOptions{
   289  				Code:         http.StatusTemporaryRedirect,
   290  				Failure:      "Your account has not been confirmed, please check your e-mail.",
   291  				RedirectPath: ab.Config.Paths.ConfirmNotOK,
   292  			}
   293  			if err := ab.Config.Core.Redirector.Redirect(w, r, ro); err != nil {
   294  				logger.Errorf("error redirecting in confirm.Middleware: #%v", err)
   295  			}
   296  		})
   297  	}
   298  }
   299  
   300  // GenerateConfirmCreds generates pieces needed for user confirm
   301  // selector: hash of the first half of a 64 byte value
   302  // (to be stored in the database and used in SELECT query)
   303  // verifier: hash of the second half of a 64 byte value
   304  // (to be stored in database but never used in SELECT query)
   305  // token: the user-facing base64 encoded selector+verifier
   306  func GenerateConfirmCreds() (selector, verifier, token string, err error) {
   307  	rawToken := make([]byte, confirmTokenSize)
   308  	if _, err = io.ReadFull(rand.Reader, rawToken); err != nil {
   309  		return "", "", "", err
   310  	}
   311  	selectorBytes := sha512.Sum512(rawToken[:confirmTokenSplit])
   312  	verifierBytes := sha512.Sum512(rawToken[confirmTokenSplit:])
   313  
   314  	return base64.StdEncoding.EncodeToString(selectorBytes[:]),
   315  		base64.StdEncoding.EncodeToString(verifierBytes[:]),
   316  		base64.URLEncoding.EncodeToString(rawToken),
   317  		nil
   318  }