github.com/volatiletech/authboss@v2.4.1+incompatible/otp/twofactor/twofactor_verify.go (about)

     1  package twofactor
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"crypto/subtle"
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"path"
    14  
    15  	"github.com/volatiletech/authboss"
    16  )
    17  
    18  // EmailVerify has a middleware function that prevents access to routes
    19  // unless e-mail has been verified.
    20  //
    21  // It does this by first setting where the user is coming from and generating
    22  // an e-mail with a random token. The token is stored in the session.
    23  //
    24  // When the user clicks the e-mail link with the token, the token is confirmed
    25  // by this middleware and the user is forwarded to the e-mail auth redirect.
    26  type EmailVerify struct {
    27  	*authboss.Authboss
    28  
    29  	TwofactorKind     string
    30  	TwofactorSetupURL string
    31  }
    32  
    33  // SetupEmailVerify registers routes for a particular 2fa method
    34  func SetupEmailVerify(ab *authboss.Authboss, twofactorKind, setupURL string) (EmailVerify, error) {
    35  	e := EmailVerify{
    36  		Authboss:          ab,
    37  		TwofactorKind:     twofactorKind,
    38  		TwofactorSetupURL: setupURL,
    39  	}
    40  
    41  	var unauthedResponse authboss.MWRespondOnFailure
    42  	if ab.Config.Modules.ResponseOnUnauthed != 0 {
    43  		unauthedResponse = ab.Config.Modules.ResponseOnUnauthed
    44  	} else if ab.Config.Modules.RoutesRedirectOnUnauthed {
    45  		unauthedResponse = authboss.RespondRedirect
    46  	}
    47  	middleware := authboss.MountedMiddleware2(ab, true, authboss.RequireFullAuth, unauthedResponse)
    48  	e.Authboss.Core.Router.Get("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.GetStart)))
    49  	e.Authboss.Core.Router.Post("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.PostStart)))
    50  
    51  	var routerMethod func(string, http.Handler)
    52  	switch ab.Config.Modules.MailRouteMethod {
    53  	case http.MethodGet:
    54  		routerMethod = ab.Core.Router.Get
    55  	case http.MethodPost:
    56  		routerMethod = ab.Core.Router.Post
    57  	default:
    58  		return e, errors.New("MailRouteMethod must be set to something in the config")
    59  	}
    60  	routerMethod("/2fa/"+twofactorKind+"/email/verify/end", middleware(ab.Core.ErrorHandler.Wrap(e.End)))
    61  
    62  	if err := e.Authboss.Core.ViewRenderer.Load(PageVerify2FA); err != nil {
    63  		return e, err
    64  	}
    65  
    66  	return e, e.Authboss.Core.MailRenderer.Load(EmailVerifyHTML, EmailVerifyTxt)
    67  }
    68  
    69  // GetStart shows the e-mail address and asks you to confirm that you would
    70  // like to proceed.
    71  func (e EmailVerify) GetStart(w http.ResponseWriter, r *http.Request) error {
    72  	cu, err := e.Authboss.CurrentUser(r)
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	user := cu.(User)
    78  
    79  	data := authboss.HTMLData{
    80  		DataVerifyEmail: user.GetEmail(),
    81  		DataVerifyURL:   path.Join(e.Authboss.Paths.Mount, "2fa", e.TwofactorKind, "email/verify"),
    82  	}
    83  	return e.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageVerify2FA, data)
    84  }
    85  
    86  // PostStart sends an e-mail and shoves the user's token into the session
    87  func (e EmailVerify) PostStart(w http.ResponseWriter, r *http.Request) error {
    88  	cu, err := e.Authboss.CurrentUser(r)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	user := cu.(User)
    94  	ctx := r.Context()
    95  	logger := e.Authboss.Logger(ctx)
    96  
    97  	token, err := GenerateToken()
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	authboss.PutSession(w, authboss.Session2FAAuthToken, token)
   103  	logger.Infof("generated new 2fa e-mail verify token for user: %s", user.GetPID())
   104  	if e.Authboss.Config.Modules.MailNoGoroutine {
   105  		e.SendVerifyEmail(ctx, user.GetEmail(), token)
   106  	} else {
   107  		go e.SendVerifyEmail(ctx, user.GetEmail(), token)
   108  	}
   109  
   110  	ro := authboss.RedirectOptions{
   111  		Code:         http.StatusTemporaryRedirect,
   112  		RedirectPath: e.Authboss.Config.Paths.TwoFactorEmailAuthNotOK,
   113  		Success:      "An e-mail has been sent to confirm 2FA activation.",
   114  	}
   115  	return e.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
   116  }
   117  
   118  // SendVerifyEmail to the user
   119  func (e EmailVerify) SendVerifyEmail(ctx context.Context, to, token string) {
   120  	logger := e.Authboss.Logger(ctx)
   121  
   122  	mailURL := e.mailURL(token)
   123  
   124  	email := authboss.Email{
   125  		To:       []string{to},
   126  		From:     e.Config.Mail.From,
   127  		FromName: e.Config.Mail.FromName,
   128  		Subject:  e.Config.Mail.SubjectPrefix + "Add 2FA to Account",
   129  	}
   130  
   131  	logger.Infof("sending add 2fa verification e-mail to: %s", to)
   132  
   133  	ro := authboss.EmailResponseOptions{
   134  		Data:         authboss.NewHTMLData(DataVerifyURL, mailURL),
   135  		HTMLTemplate: EmailVerifyHTML,
   136  		TextTemplate: EmailVerifyTxt,
   137  	}
   138  	if err := e.Authboss.Email(ctx, email, ro); err != nil {
   139  		logger.Errorf("failed to send 2fa verification e-mail to %s: %+v", to, err)
   140  	}
   141  }
   142  
   143  func (e EmailVerify) mailURL(token string) string {
   144  	query := url.Values{FormValueToken: []string{token}}
   145  
   146  	if len(e.Config.Mail.RootURL) != 0 {
   147  		return fmt.Sprintf("%s?%s",
   148  			e.Config.Mail.RootURL+"/2fa/"+e.TwofactorKind+"/email/verify/end",
   149  			query.Encode())
   150  	}
   151  
   152  	p := path.Join(e.Config.Paths.Mount, "/2fa/"+e.TwofactorKind+"/email/verify/end")
   153  	return fmt.Sprintf("%s%s?%s", e.Config.Paths.RootURL, p, query.Encode())
   154  }
   155  
   156  // End confirms the token passed in by the user (by the link in the e-mail)
   157  func (e EmailVerify) End(w http.ResponseWriter, r *http.Request) error {
   158  	values, err := e.Authboss.Core.BodyReader.Read(PageVerifyEnd2FA, r)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	tokenValues := MustHaveEmailVerifyTokenValues(values)
   164  	wantToken := tokenValues.GetToken()
   165  
   166  	givenToken, _ := authboss.GetSession(r, authboss.Session2FAAuthToken)
   167  
   168  	if 1 != subtle.ConstantTimeCompare([]byte(wantToken), []byte(givenToken)) {
   169  		ro := authboss.RedirectOptions{
   170  			Code:         http.StatusTemporaryRedirect,
   171  			Failure:      "invalid 2fa e-mail verification token",
   172  			RedirectPath: e.Authboss.Config.Paths.TwoFactorEmailAuthNotOK,
   173  		}
   174  		return e.Authboss.Core.Redirector.Redirect(w, r, ro)
   175  	}
   176  
   177  	authboss.DelSession(w, authboss.Session2FAAuthToken)
   178  	authboss.PutSession(w, authboss.Session2FAAuthed, "true")
   179  
   180  	ro := authboss.RedirectOptions{
   181  		Code:         http.StatusTemporaryRedirect,
   182  		RedirectPath: e.TwofactorSetupURL,
   183  	}
   184  	return e.Authboss.Core.Redirector.Redirect(w, r, ro)
   185  }
   186  
   187  // Wrap a route and stop it from being accessed unless the Session2FAAuthed
   188  // session value is "true".
   189  func (e EmailVerify) Wrap(handler http.Handler) http.Handler {
   190  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   191  		if !e.Authboss.Config.Modules.TwoFactorEmailAuthRequired {
   192  			handler.ServeHTTP(w, r)
   193  			return
   194  		}
   195  
   196  		// If this value exists the user's already verified
   197  		authed, _ := authboss.GetSession(r, authboss.Session2FAAuthed)
   198  		if authed == "true" {
   199  			handler.ServeHTTP(w, r)
   200  			return
   201  		}
   202  
   203  		redirURL := path.Join(e.Authboss.Config.Paths.Mount, "2fa", e.TwofactorKind, "email/verify")
   204  		ro := authboss.RedirectOptions{
   205  			Code:         http.StatusTemporaryRedirect,
   206  			Failure:      "You must first authorize adding 2fa by e-mail.",
   207  			RedirectPath: redirURL,
   208  		}
   209  
   210  		if err := e.Authboss.Core.Redirector.Redirect(w, r, ro); err != nil {
   211  			logger := e.Authboss.RequestLogger(r)
   212  			logger.Errorf("failed to redirect client: %+v", err)
   213  			return
   214  		}
   215  	})
   216  }
   217  
   218  // EmailVerifyTokenValuer returns a token from the body
   219  type EmailVerifyTokenValuer interface {
   220  	authboss.Validator
   221  
   222  	GetToken() string
   223  }
   224  
   225  // MustHaveEmailVerifyTokenValues upgrades a validatable set of values
   226  // to ones specific to a user that needs to be recovered.
   227  func MustHaveEmailVerifyTokenValues(v authboss.Validator) EmailVerifyTokenValuer {
   228  	if u, ok := v.(EmailVerifyTokenValuer); ok {
   229  		return u
   230  	}
   231  
   232  	panic(fmt.Sprintf("bodyreader returned a type that could not be upgraded to an EmailVerifyTokenValues: %T", v))
   233  }
   234  
   235  // GenerateToken used for authenticating e-mails for 2fa setup
   236  func GenerateToken() (string, error) {
   237  	rawToken := make([]byte, verifyEmailTokenSize)
   238  	if _, err := io.ReadFull(rand.Reader, rawToken); err != nil {
   239  		return "", err
   240  	}
   241  
   242  	return base64.URLEncoding.EncodeToString(rawToken), nil
   243  }