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 }