auth

Paddy 2015-01-28 Parent:23c1a07c8a61 Child:d30a3a12d387

132:163ce22fa4c9 Go to Latest

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.

History
     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")