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  }