auth
auth/session.go
Enable CSRF protection, add expiration to sessions. Sessions gain a CSRF token, which is passed as a parameter to the login page. The login page now checks for that CSRF token, and logs a CSRF attempt if the token does not match. I also added an expiration to sessions, so they don't last forever. Sessions should be pretty short--we just need to stay logged in for long enough to approve the OAuth request. Everything after that should be cookie based. Finally, I added a configuration parameter to control whether the session cookie should be set to Secure, requiring the use of HTTPS. For production use, this flag is a requirement, but it makes testing extremely difficult, so we need a way to disable it.
1.1 --- a/session.go Sat Jan 24 10:34:33 2015 -0500 1.2 +++ b/session.go Wed Jan 28 07:27:32 2015 -0500 1.3 @@ -1,7 +1,9 @@ 1.4 package auth 1.5 1.6 import ( 1.7 + "crypto/rand" 1.8 "crypto/sha256" 1.9 + "encoding/base64" 1.10 "encoding/hex" 1.11 "encoding/json" 1.12 "errors" 1.13 @@ -16,6 +18,7 @@ 1.14 ) 1.15 1.16 const ( 1.17 + authCookieName = "auth" 1.18 loginTemplateName = "login" 1.19 ) 1.20 1.21 @@ -38,6 +41,8 @@ 1.22 ErrInvalidSession = errors.New("session is not valid") 1.23 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore. 1.24 ErrSessionAlreadyExists = errors.New("session already exists") 1.25 + // ErrCSRFAttempt is returned when a CSRF attempt is detected. 1.26 + ErrCSRFAttempt = errors.New("CSRF attempt") 1.27 1.28 passphraseSchemes = map[int]passphraseScheme{ 1.29 1: { 1.30 @@ -63,7 +68,9 @@ 1.31 ProfileID uuid.ID 1.32 Login string 1.33 Created time.Time 1.34 + Expires time.Time 1.35 Active bool 1.36 + CSRFToken string 1.37 } 1.38 1.39 type sortedSessions []Session 1.40 @@ -145,6 +152,13 @@ 1.41 // BUG(paddy): We need to implement a handler for terminating sessions. 1.42 } 1.43 1.44 +func checkCSRF(r *http.Request, s Session) error { 1.45 + if r.PostFormValue("csrftoken") != s.CSRFToken { 1.46 + return ErrCSRFAttempt 1.47 + } 1.48 + return nil 1.49 +} 1.50 + 1.51 func checkCookie(r *http.Request, context Context) (Session, error) { 1.52 cookie, err := r.Cookie(authCookieName) 1.53 if err == http.ErrNoCookie { 1.54 @@ -162,6 +176,9 @@ 1.55 if !sess.Active { 1.56 return Session{}, ErrInvalidSession 1.57 } 1.58 + if time.Now().After(sess.Expires) { 1.59 + return Session{}, ErrInvalidSession 1.60 + } 1.61 return sess, nil 1.62 } 1.63 1.64 @@ -233,7 +250,6 @@ 1.65 1.66 // CreateSessionHandler allows the user to log into their account and create their session. 1.67 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.68 - // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit 1.69 errors := []error{} 1.70 if r.Method == "POST" { 1.71 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context) 1.72 @@ -242,14 +258,32 @@ 1.73 if ip == "" { 1.74 ip = r.RemoteAddr 1.75 } 1.76 + sessionID := make([]byte, 32) 1.77 + csrfToken := make([]byte, 32) 1.78 + _, err = rand.Read(sessionID) 1.79 + if err != nil { 1.80 + log.Println("Error reading CSPRNG for session ID:", err) 1.81 + w.WriteHeader(http.StatusInternalServerError) 1.82 + w.Write([]byte("Internal error")) 1.83 + return 1.84 + } 1.85 + _, err = rand.Read(csrfToken) 1.86 + if err != nil { 1.87 + log.Println("Error reading CSPRNG for CSRF token:", err) 1.88 + w.WriteHeader(http.StatusInternalServerError) 1.89 + w.Write([]byte("internal error")) 1.90 + return 1.91 + } 1.92 session := Session{ 1.93 - ID: uuid.NewID().String(), 1.94 + ID: base64.StdEncoding.EncodeToString(sessionID), 1.95 IP: ip, 1.96 UserAgent: r.UserAgent(), 1.97 ProfileID: profile.ID, 1.98 Login: r.PostFormValue("login"), 1.99 Created: time.Now(), 1.100 + Expires: time.Now().Add(time.Hour), 1.101 Active: true, 1.102 + CSRFToken: base64.StdEncoding.EncodeToString(csrfToken), 1.103 } 1.104 err = context.CreateSession(session) 1.105 if err != nil { 1.106 @@ -257,12 +291,13 @@ 1.107 w.Write([]byte(err.Error())) 1.108 return 1.109 } 1.110 - // BUG(paddy): We really need to do a security audit on our cookie. 1.111 + // BUG(paddy): We really need to do a security audit on our cookies. 1.112 cookie := http.Cookie{ 1.113 Name: authCookieName, 1.114 Value: session.ID, 1.115 - Expires: time.Now().Add(24 * 7 * time.Hour), 1.116 + Expires: session.Expires, 1.117 HttpOnly: true, 1.118 + Secure: context.config.secureCookie, 1.119 } 1.120 http.SetCookie(w, &cookie) 1.121 redirectTo := r.URL.Query().Get("from")