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 }