func createSession(w http.ResponseWriter, r *http.Request, session *sessions.Session) *ServerSession { // Each session needs a unique ID in order to be saved. if session.ID == "" { session.ID = tokens.NewSessionID() } ss := &ServerSession{ CSRFToken: tokens.NewCSRFToken(session.ID), } // Attempt to store the session. Remove the session if it's not stored // correctly. if err := ss.StoreSession(session.ID); err != nil { RemoveSession(session.ID) glog.Fatalln(err) } // Similarly, save it in our FS storage and set the user's cookie. if err := session.Save(r, w); err != nil { RemoveSession(session.ID) glog.Fatalln(err) } return ss }
// AuthenticateUser attempts to authenticate a user from the login page. // // It returns: // - An an enum indicating the user's authentication status. // - A pointer to dt.LoginData which holds information to be displayed. // - An HTTPError if applicable. // // Errors displayed to the user _must_ use proper punctuation. func AuthenticateUser(w http.ResponseWriter, r *http.Request) (AuthStatus, *dt.LoginData, *HTTPError) { // Gather information about the user. session, validAuth, httperr := CheckSession(r) if httperr != nil { glog.Errorln(httperr.Err) return InvalidAuth, &dt.LoginData{}, &HTTPError{ http.StatusInternalServerError, httperr.Err, } } ss, err := GetSession(session.ID) if err != nil { glog.Errorln(err) } // Technically this should never be true because if the cookie is // valid the user won't have to re-authenticate. if validAuth { glog.V(2).Infof("redirected to %s\n", paths.DashboardPath) return ValidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, Redir: paths.DashboardPath, }, nil } // Now we can begin to gather form information. if err := r.ParseForm(); err != nil { glog.Errorln(err) return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, }, &HTTPError{http.StatusInternalServerError, err} } if !ValidCSRF(r, session, false) { return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, Error: "Invalid CSRF token.", }, &HTTPError{http.StatusOK, ErrInvalidCSRFToken} } // Max email address/username is 255 characters, so if it's too long bail. // There's no reason to _not_ have a hard limit for email addresses. formID := r.PostFormValue("username") length, ok := validLoginInput(formID) // Username with a length of 0. if !length { return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, Error: "Enter your username.", }, &HTTPError{http.StatusOK, ErrInvalidLogin} } if !ok { return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, Error: "Enter a valid username.", }, &HTTPError{http.StatusOK, ErrInvalidLogin} } // No need to validate size. POSTs larger than client_max_body_size will // be discarded, and there's absolutely no reason why we should _ever_ // have a limit on password length. formPassword := r.PostFormValue("password") // Now that the form data isn't blatantly invalid, create a new user // object to test the form data given to us. user, exists, err := database.CheckUser(formID) if err != nil { glog.Errorln(err) } // User does not exist. Package the error and display an error message. if !exists { session.AddFlash(ErrBadUsername.Error(), "_errors") if err := session.Save(r, w); err != nil { glog.Errorln(err) return InvalidAuth, &dt.LoginData{CSRF: ss.CSRFToken}, &HTTPError{ http.StatusInternalServerError, err, } } // NOTE: It's _not_ a security risk to let the user know the given // username does not exist. If there's a public sign up page, then // they already have the ability to check if a username exists, // thus any "coy" language like, "The username/password are invalid" // just creates a bad user experience. // See: http://blog.codinghorror.com/the-god-login/ return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, Error: "Username does not exist.", }, &HTTPError{http.StatusOK, ErrBadUsername} } // Securely compare hashes. err = ComparePassword(user, []byte(formPassword), true) if err != nil { glog.Errorln(err) session.AddFlash(ErrBadPassword.Error(), "_errors") if err := session.Save(r, w); err != nil { glog.Errorln(err) return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, }, &HTTPError{ http.StatusInternalServerError, ErrBadPassword, } } // bcrypt.CompareHashAndPassword will return one of two named errors: // - ErrMismatchedHashAndPasword if the password doesn't match // the hash. // - ErrHashTooShort if the hash is too short to be a bcrypt hash if err == bcrypt.ErrMismatchedHashAndPassword { return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, Error: "The password you entered is incorrect.", }, &HTTPError{http.StatusOK, ErrBadPassword} } else { glog.Errorln(err) return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken}, &HTTPError{ http.StatusInternalServerError, err, } } } // Default end date is now (ns) + our allowance, which is usually like // 8 hours or so. endDate := time.Now().UnixNano() + allowance // If the user selected the "remember me" checkbox, set the expiration // date to as high as it will go. remember := r.PostFormValue("remember") if remember == "true" { endDate = int64(^uint64(0) >> 1) } // Check if the CSRF/ID are set. It might not be set if the user didn't // have any cookies when they visited the login page. if session.ID == "" { session.ID = tokens.NewSessionID() } csrf, _ := getCSRF(session) ss.AuthToken = tokens.NewAuthToken() ss.CSRFToken = csrf ss.Email = user.Email ss.Date = endDate ss.School = user.School // Store the session and if it doesn't work try to remove it just // to be safe. // // (Note: this does *not* save the session in the user's // browser. While a more generic 'SaveSession' would be nice, it'd // also be mixing our auth and SQL 'modules' which is a bit sloppy. // I'd rather have to explicitly perform each step rather than // wade through a function that abstracts away arguably the most // important logic in the app.) err = ss.StoreSession(session.ID) if err != nil { RemoveSession(session.ID) glog.Errorln(err) return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, }, &HTTPError{http.StatusInternalServerError, err} } // Store some relevant values. session.Values[authToken] = ss.AuthToken session.Values[authDate] = ss.Date session.Values[csrfToken] = ss.CSRFToken session.Values["user"] = ss.Email session.Values["school"] = ss.School // Set cookie termination date for responsible clients. // Adjust ns to seconds because MaxAge assumes seconds. session.Options.MaxAge = int(endDate / nsConv) // Now we save the session in the user's browser. err = session.Save(r, w) if err != nil { glog.Errorln(err) return InvalidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, }, &HTTPError{http.StatusInternalServerError, err} } // Send the user to the dashboard. return ValidAuth, &dt.LoginData{ CSRF: ss.CSRFToken, Redir: paths.DashboardPath, }, nil }