github.com/volatiletech/authboss@v2.4.1+incompatible/otp/otp.go (about) 1 // Package otp allows authentication through a one time password 2 // instead of a traditional password. 3 package otp 4 5 import ( 6 "context" 7 "crypto/rand" 8 "crypto/sha512" 9 "crypto/subtle" 10 "encoding/base64" 11 "fmt" 12 "io" 13 "net/http" 14 "strconv" 15 "strings" 16 17 "github.com/pkg/errors" 18 "github.com/volatiletech/authboss" 19 ) 20 21 const ( 22 otpSize = 16 23 maxOTPs = 5 24 25 // PageLogin is for identifying the login page for parsing & validation 26 PageLogin = "otplogin" 27 // PageAdd is for adding an otp to the user 28 PageAdd = "otpadd" 29 // PageClear is for deleting all the otps from the user 30 PageClear = "otpclear" 31 32 // DataNumberOTPs shows the number of otps for add/clear operations 33 DataNumberOTPs = "otp_count" 34 // DataOTP shows the new otp that was added 35 DataOTP = "otp" 36 ) 37 38 // User for one time passwords 39 type User interface { 40 authboss.User 41 42 // GetOTPs retrieves a string of comma separated bcrypt'd one time passwords 43 GetOTPs() string 44 // PutOTPs puts a string of comma separated bcrypt'd one time passwords 45 PutOTPs(string) 46 } 47 48 // MustBeOTPable ensures the user can use one time passwords 49 func MustBeOTPable(user authboss.User) User { 50 u, ok := user.(User) 51 if !ok { 52 panic(fmt.Sprintf("could not upgrade user to an otpable user, type: %T", u)) 53 } 54 55 return u 56 } 57 58 func init() { 59 authboss.RegisterModule("otp", &OTP{}) 60 } 61 62 // OTP module 63 type OTP struct { 64 *authboss.Authboss 65 } 66 67 // Init module 68 func (o *OTP) Init(ab *authboss.Authboss) (err error) { 69 o.Authboss = ab 70 71 if err = o.Authboss.Config.Core.ViewRenderer.Load(PageLogin, PageAdd, PageClear); err != nil { 72 return err 73 } 74 75 o.Authboss.Config.Core.Router.Get("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginGet)) 76 o.Authboss.Config.Core.Router.Post("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginPost)) 77 78 var unauthedResponse authboss.MWRespondOnFailure 79 if ab.Config.Modules.ResponseOnUnauthed != 0 { 80 unauthedResponse = ab.Config.Modules.ResponseOnUnauthed 81 } else if ab.Config.Modules.RoutesRedirectOnUnauthed { 82 unauthedResponse = authboss.RespondRedirect 83 } 84 middleware := authboss.MountedMiddleware2(ab, true, authboss.RequireNone, unauthedResponse) 85 o.Authboss.Config.Core.Router.Get("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddGet))) 86 o.Authboss.Config.Core.Router.Post("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddPost))) 87 88 o.Authboss.Config.Core.Router.Get("/otp/clear", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.ClearGet))) 89 o.Authboss.Config.Core.Router.Post("/otp/clear", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.ClearPost))) 90 91 return nil 92 } 93 94 // LoginGet simply displays the login form 95 func (o *OTP) LoginGet(w http.ResponseWriter, r *http.Request) error { 96 var data authboss.HTMLData 97 if redir := r.URL.Query().Get(authboss.FormValueRedirect); len(redir) != 0 { 98 data = authboss.HTMLData{authboss.FormValueRedirect: redir} 99 } 100 return o.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data) 101 } 102 103 // LoginPost attempts to validate the credentials passed in 104 // to log in a user. 105 func (o *OTP) LoginPost(w http.ResponseWriter, r *http.Request) error { 106 logger := o.RequestLogger(r) 107 108 validatable, err := o.Authboss.Core.BodyReader.Read(PageLogin, r) 109 if err != nil { 110 return err 111 } 112 113 // Skip validation since all the validation happens during the database lookup and 114 // password check. 115 creds := authboss.MustHaveUserValues(validatable) 116 117 pid := creds.GetPID() 118 pidUser, err := o.Authboss.Storage.Server.Load(r.Context(), pid) 119 if err == authboss.ErrUserNotFound { 120 logger.Infof("failed to load user requested by pid: %s", pid) 121 data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"} 122 return o.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data) 123 } else if err != nil { 124 return err 125 } 126 127 otpUser := MustBeOTPable(pidUser) 128 passwords := splitOTPs(otpUser.GetOTPs()) 129 130 r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, pidUser)) 131 132 inputSum := sha512.Sum512([]byte(creds.GetPassword())) 133 matchPassword := -1 134 for i, p := range passwords { 135 dbSum, err := base64.StdEncoding.DecodeString(p) 136 if err != nil { 137 return errors.Wrap(err, "otp in database was not valid base64") 138 } 139 140 if 1 == subtle.ConstantTimeCompare(inputSum[:], dbSum) { 141 matchPassword = i 142 break 143 } 144 } 145 146 var handled bool 147 if matchPassword < 0 { 148 handled, err = o.Authboss.Events.FireAfter(authboss.EventAuthFail, w, r) 149 if err != nil { 150 return err 151 } else if handled { 152 return nil 153 } 154 155 logger.Infof("user %s failed to log in with otp", pid) 156 data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"} 157 return o.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data) 158 } 159 160 logger.Infof("removing otp password from %s", pid) 161 passwords[matchPassword] = passwords[len(passwords)-1] 162 passwords = passwords[:len(passwords)-1] 163 otpUser.PutOTPs(joinOTPs(passwords)) 164 if err = o.Authboss.Config.Storage.Server.Save(r.Context(), pidUser); err != nil { 165 return err 166 } 167 168 r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyValues, validatable)) 169 170 handled, err = o.Events.FireBefore(authboss.EventAuth, w, r) 171 if err != nil { 172 return err 173 } else if handled { 174 return nil 175 } 176 177 handled, err = o.Events.FireBefore(authboss.EventAuthHijack, w, r) 178 if err != nil { 179 return err 180 } else if handled { 181 return nil 182 } 183 184 logger.Infof("user %s logged in via otp", pid) 185 authboss.PutSession(w, authboss.SessionKey, pid) 186 authboss.DelSession(w, authboss.SessionHalfAuthKey) 187 188 handled, err = o.Authboss.Events.FireAfter(authboss.EventAuth, w, r) 189 if err != nil { 190 return err 191 } else if handled { 192 return nil 193 } 194 195 ro := authboss.RedirectOptions{ 196 Code: http.StatusTemporaryRedirect, 197 RedirectPath: o.Authboss.Paths.AuthLoginOK, 198 FollowRedirParam: true, 199 } 200 return o.Authboss.Core.Redirector.Redirect(w, r, ro) 201 } 202 203 // AddGet shows how many passwords exist and allows the user to create a new one 204 func (o *OTP) AddGet(w http.ResponseWriter, r *http.Request) error { 205 return o.showOTPCount(w, r, PageAdd) 206 } 207 208 // AddPost adds a new password to the user and displays it 209 func (o *OTP) AddPost(w http.ResponseWriter, r *http.Request) error { 210 logger := o.RequestLogger(r) 211 212 user, err := o.Authboss.CurrentUser(r) 213 if err != nil { 214 return err 215 } 216 217 otpUser := MustBeOTPable(user) 218 currentOTPs := splitOTPs(otpUser.GetOTPs()) 219 220 if len(currentOTPs) >= maxOTPs { 221 data := authboss.HTMLData{authboss.DataValidation: fmt.Sprintf("you cannot have more than %d one time passwords", maxOTPs)} 222 return o.Core.Responder.Respond(w, r, http.StatusOK, PageAdd, data) 223 } 224 225 logger.Infof("generating otp for %s", user.GetPID()) 226 otp, hash, err := generateOTP() 227 if err != nil { 228 return err 229 } 230 231 currentOTPs = append(currentOTPs, hash) 232 otpUser.PutOTPs(joinOTPs(currentOTPs)) 233 234 if err := o.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil { 235 return err 236 } 237 238 return o.Core.Responder.Respond(w, r, http.StatusOK, PageAdd, authboss.HTMLData{DataOTP: otp}) 239 } 240 241 // ClearGet shows how many passwords exist and allows the user to clear them all 242 func (o *OTP) ClearGet(w http.ResponseWriter, r *http.Request) error { 243 return o.showOTPCount(w, r, PageClear) 244 } 245 246 // ClearPost clears all otps that are stored for the user. 247 func (o *OTP) ClearPost(w http.ResponseWriter, r *http.Request) error { 248 logger := o.RequestLogger(r) 249 250 user, err := o.Authboss.CurrentUser(r) 251 if err != nil { 252 return err 253 } 254 255 logger.Infof("clearing all otps for user: %s", user.GetPID()) 256 otpUser := MustBeOTPable(user) 257 otpUser.PutOTPs("") 258 259 if err := o.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil { 260 return err 261 } 262 263 return o.Core.Responder.Respond(w, r, http.StatusOK, PageAdd, authboss.HTMLData{DataNumberOTPs: "0"}) 264 } 265 266 func (o *OTP) showOTPCount(w http.ResponseWriter, r *http.Request, page string) error { 267 user, err := o.Authboss.CurrentUser(r) 268 if err != nil { 269 return err 270 } 271 272 otpUser := MustBeOTPable(user) 273 ln := strconv.Itoa(len(splitOTPs(otpUser.GetOTPs()))) 274 275 return o.Core.Responder.Respond(w, r, http.StatusOK, page, authboss.HTMLData{DataNumberOTPs: ln}) 276 } 277 278 func joinOTPs(otps []string) string { 279 return strings.Join(otps, ",") 280 } 281 282 func splitOTPs(otps string) []string { 283 if len(otps) == 0 { 284 return nil 285 } 286 287 return strings.Split(otps, ",") 288 } 289 290 func generateOTP() (otp string, hash string, err error) { 291 secret := make([]byte, otpSize) 292 if _, err = io.ReadFull(rand.Reader, secret); err != nil { 293 return "", "", err 294 } 295 296 otp = fmt.Sprintf("%x-%x-%x-%x", 297 secret[0:4], 298 secret[4:8], 299 secret[8:12], 300 secret[12:16], 301 ) 302 303 sum := sha512.Sum512([]byte(otp)) 304 encoded := make([]byte, base64.StdEncoding.EncodedLen(sha512.Size)) 305 base64.StdEncoding.Encode(encoded, sum[:]) 306 hash = string(encoded) 307 308 return otp, hash, nil 309 }