From 62abc111429446f4e0516671d9c2f787088391e4 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 16 Oct 2018 20:30:38 -0400 Subject: [PATCH] Add web session management --- app.go | 2 + errors.go | 11 +++++ keys.go | 7 ++- session.go | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++ users.go | 51 ++++++++++++++++++++++ 5 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 errors.go create mode 100644 session.go create mode 100644 users.go diff --git a/app.go b/app.go index f51908c..669c751 100644 --- a/app.go +++ b/app.go @@ -60,6 +60,8 @@ func Serve() { } // Initialize modules + app.sessionStore = initSession(app) + r := mux.NewRouter() handler := NewHandler(app.sessionStore) diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..28819fb --- /dev/null +++ b/errors.go @@ -0,0 +1,11 @@ +package writefreely + +import ( + "github.com/writeas/impart" + "net/http" +) + +// Commonly returned HTTP errors +var ( + ErrInternalCookieSession = impart.HTTPError{http.StatusInternalServerError, "Could not get cookie session."} +) diff --git a/keys.go b/keys.go index 505141e..3f594f5 100644 --- a/keys.go +++ b/keys.go @@ -5,13 +5,18 @@ import ( ) type keychain struct { - cookieAuthKey, cookieKey []byte + emailKey, cookieAuthKey, cookieKey []byte } func initKeys(app *app) error { var err error app.keys = &keychain{} + app.keys.emailKey, err = ioutil.ReadFile("keys/email.aes256") + if err != nil { + return err + } + app.keys.cookieAuthKey, err = ioutil.ReadFile("keys/cookies_auth.aes256") if err != nil { return err diff --git a/session.go b/session.go new file mode 100644 index 0000000..bd768cb --- /dev/null +++ b/session.go @@ -0,0 +1,126 @@ +package writefreely + +import ( + "encoding/gob" + "github.com/gorilla/sessions" + "github.com/writeas/web-core/log" + "net/http" + "strings" +) + +const ( + day = 86400 + sessionLength = 180 * day + cookieName = "wfu" + cookieUserVal = "u" +) + +// initSession creates the cookie store. It depends on the keychain already +// being loaded. +func initSession(app *app) *sessions.CookieStore { + // Register complex data types we'll be storing in cookies + gob.Register(&User{}) + + // Create the cookie store + store := sessions.NewCookieStore(app.keys.cookieAuthKey, app.keys.cookieKey) + store.Options = &sessions.Options{ + Path: "/", + MaxAge: sessionLength, + HttpOnly: true, + Secure: strings.HasPrefix(app.cfg.Server.Host, "https://"), + } + return store +} + +func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) { + var err error + if session == nil { + session, err = app.sessionStore.Get(r, cookieName) + if err != nil { + return nil, err + } + } + + f := []string{} + if flashes := session.Flashes(); len(flashes) > 0 { + for _, flash := range flashes { + if str, ok := flash.(string); ok { + f = append(f, str) + } + } + } + saveUserSession(app, r, w) + + return f, nil +} + +func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error { + var err error + if session == nil { + session, err = app.sessionStore.Get(r, cookieName) + } + + if err != nil { + log.Error("Unable to add flash '%s': %v", m, err) + return err + } + + session.AddFlash(m) + saveUserSession(app, r, w) + return nil +} + +func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) { + session, err := app.sessionStore.Get(r, cookieName) + if err == nil { + // Got the currently logged-in user + val := session.Values[cookieUserVal] + var u = &User{} + var ok bool + if u, ok = val.(*User); ok { + return u, session + } + } + + return nil, nil +} + +func getUserSession(app *app, r *http.Request) *User { + u, _ := getUserAndSession(app, r) + return u +} + +func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error { + session, err := app.sessionStore.Get(r, cookieName) + if err != nil { + return ErrInternalCookieSession + } + + // Extend the session + session.Options.MaxAge = int(sessionLength) + + // Remove any information that accidentally got added + // FIXME: find where Plan information is getting saved to cookie. + val := session.Values[cookieUserVal] + var u = &User{} + var ok bool + if u, ok = val.(*User); ok { + session.Values[cookieUserVal] = u.Cookie() + } + + err = session.Save(r, w) + if err != nil { + log.Error("Couldn't saveUserSession: %v", err) + } + return err +} + +func getFullUserSession(app *app, r *http.Request) *User { + u := getUserSession(app, r) + if u == nil { + return nil + } + + u, _ = app.db.GetUserByID(u.ID) + return u +} diff --git a/users.go b/users.go new file mode 100644 index 0000000..e9e63a5 --- /dev/null +++ b/users.go @@ -0,0 +1,51 @@ +package writefreely + +import ( + "time" + + "github.com/guregu/null/zero" + "github.com/writeas/web-core/data" + "github.com/writeas/web-core/log" +) + +type ( + // User is a consistent user object in the database and all contexts (auth + // and non-auth) in the API. + User struct { + ID int64 `json:"-"` + Username string `json:"username"` + HashedPass []byte `json:"-"` + HasPass bool `json:"has_pass"` + Email zero.String `json:"email"` + Created time.Time `json:"created"` + + clearEmail string `json:"email"` + } +) + +// EmailClear decrypts and returns the user's email, caching it in the user +// object. +func (u *User) EmailClear(keys *keychain) string { + if u.clearEmail != "" { + return u.clearEmail + } + + if u.Email.Valid && u.Email.String != "" { + email, err := data.Decrypt(keys.emailKey, []byte(u.Email.String)) + if err != nil { + log.Error("Error decrypting user email: %v", err) + } else { + u.clearEmail = string(email) + return u.clearEmail + } + } + return "" +} + +// Cookie strips down an AuthUser to contain only information necessary for +// cookies. +func (u User) Cookie() *User { + u.HashedPass = []byte{} + + return &u +}