auth
auth/session.go
Move login concerns to session, add login handler. Move all our helpers for authenticating, building a login redirect, and reading a cookie to session.go. Rewrite our passphrase scheme code so that a scheme is just a struct with three functions for checking a passphrase against a profile object, generating a passphrase, and calculating the number of iterations to use when generating a passphrase. Define an implementation of our passphrase scheme (scheme #1) using PBKDF2 and SHA256. Add a CreateSessionHandler function that logs the user in using their login and passphrase. Add a RegisterSessionHandlers function that adds the session-related handlers (right now, just our CreateSessionHandler) to the specified router.
1.1 --- a/session.go Sun Dec 14 12:01:44 2014 -0500 1.2 +++ b/session.go Sun Dec 14 12:05:38 2014 -0500 1.3 @@ -1,11 +1,22 @@ 1.4 package auth 1.5 1.6 import ( 1.7 + "crypto/sha256" 1.8 + "encoding/hex" 1.9 "errors" 1.10 + "log" 1.11 + "net/http" 1.12 "sort" 1.13 "time" 1.14 1.15 + "code.secondbit.org/pass" 1.16 "code.secondbit.org/uuid" 1.17 + 1.18 + "github.com/gorilla/mux" 1.19 +) 1.20 + 1.21 +const ( 1.22 + loginTemplateName = "login" 1.23 ) 1.24 1.25 var ( 1.26 @@ -17,8 +28,22 @@ 1.27 ErrInvalidSession = errors.New("session is not valid") 1.28 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore. 1.29 ErrSessionAlreadyExists = errors.New("session already exists") 1.30 + 1.31 + passphraseSchemes = map[int]passphraseScheme{ 1.32 + 1: { 1.33 + check: pbkdf2sha256check, 1.34 + create: pbkdf2sha256create, 1.35 + calculateIterations: pbkdf2sha256calc, 1.36 + }, 1.37 + } 1.38 ) 1.39 1.40 +type passphraseScheme struct { 1.41 + check func(profile Profile, passphrase string) (bool, error) 1.42 + create func(passphrase string, iterations int) (result, salt []byte, err error) 1.43 + calculateIterations func() (int, error) 1.44 +} 1.45 + 1.46 // Session represents a user's authenticated session, associating it with a profile 1.47 // and some audit data. 1.48 type Session struct { 1.49 @@ -26,8 +51,8 @@ 1.50 IP string 1.51 UserAgent string 1.52 ProfileID uuid.ID 1.53 + Login string 1.54 Created time.Time 1.55 - Login string 1.56 Active bool 1.57 } 1.58 1.59 @@ -102,3 +127,137 @@ 1.60 res = []Session(sorted) 1.61 return res, nil 1.62 } 1.63 + 1.64 +// RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout. 1.65 +func RegisterSessionHandlers(r *mux.Router, context Context) { 1.66 + r.Handle("/login", wrap(context, CreateSessionHandler)) 1.67 +} 1.68 + 1.69 +func checkCookie(r *http.Request, context Context) (Session, error) { 1.70 + cookie, err := r.Cookie(authCookieName) 1.71 + if err == http.ErrNoCookie { 1.72 + return Session{}, ErrNoSession 1.73 + } else if err != nil { 1.74 + log.Println(err) 1.75 + return Session{}, err 1.76 + } 1.77 + sess, err := context.GetSession(cookie.Value) 1.78 + if err == ErrSessionNotFound { 1.79 + return Session{}, ErrInvalidSession 1.80 + } else if err != nil { 1.81 + return Session{}, err 1.82 + } 1.83 + if !sess.Active { 1.84 + return Session{}, ErrInvalidSession 1.85 + } 1.86 + return sess, nil 1.87 +} 1.88 + 1.89 +func buildLoginRedirect(r *http.Request, context Context) string { 1.90 + if context.loginURI == nil { 1.91 + return "" 1.92 + } 1.93 + uri := *context.loginURI 1.94 + q := uri.Query() 1.95 + q.Set("from", r.URL.String()) 1.96 + uri.RawQuery = q.Encode() 1.97 + return uri.String() 1.98 +} 1.99 + 1.100 +func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) { 1.101 + realPass, err := hex.DecodeString(profile.Passphrase) 1.102 + if err != nil { 1.103 + return false, err 1.104 + } 1.105 + candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt)) 1.106 + if !pass.Compare(candidate, realPass) { 1.107 + return false, ErrIncorrectAuth 1.108 + } 1.109 + return true, nil 1.110 +} 1.111 + 1.112 +func pbkdf2sha256create(passphrase string, iters int) (result, salt []byte, err error) { 1.113 + return pass.Create(sha256.New, iters, []byte(passphrase)) 1.114 +} 1.115 + 1.116 +func pbkdf2sha256calc() (int, error) { 1.117 + return pass.CalculateIterations(sha256.New) 1.118 +} 1.119 + 1.120 +func authenticate(user, passphrase string, context Context) (Profile, error) { 1.121 + profile, err := context.GetProfileByLogin(user) 1.122 + if err != nil { 1.123 + if err == ErrProfileNotFound || err == ErrLoginNotFound { 1.124 + return Profile{}, ErrIncorrectAuth 1.125 + } 1.126 + return Profile{}, err 1.127 + } 1.128 + if profile.Compromised { 1.129 + return Profile{}, ErrProfileCompromised 1.130 + } 1.131 + if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) { 1.132 + return profile, ErrProfileLocked 1.133 + } 1.134 + scheme, ok := passphraseSchemes[profile.PassphraseScheme] 1.135 + if !ok { 1.136 + return Profile{}, ErrInvalidPassphraseScheme 1.137 + } 1.138 + result, err := scheme.check(profile, passphrase) 1.139 + if !result { 1.140 + return Profile{}, err 1.141 + } 1.142 + return profile, nil 1.143 +} 1.144 + 1.145 +// CreateSessionHandler allows the user to log into their account and create their session. 1.146 +func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.147 + // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit 1.148 + errors := []error{} 1.149 + if r.Method == "POST" { 1.150 + profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context) 1.151 + if err == nil { 1.152 + ip := r.Header.Get("X-Forwarded-For") 1.153 + if ip == "" { 1.154 + ip = r.RemoteAddr 1.155 + } 1.156 + session := Session{ 1.157 + ID: uuid.NewID().String(), 1.158 + IP: ip, 1.159 + UserAgent: r.UserAgent(), 1.160 + ProfileID: profile.ID, 1.161 + Login: r.PostFormValue("login"), 1.162 + Created: time.Now(), 1.163 + Active: true, 1.164 + } 1.165 + err = context.CreateSession(session) 1.166 + if err != nil { 1.167 + w.WriteHeader(http.StatusInternalServerError) 1.168 + w.Write([]byte(err.Error())) 1.169 + return 1.170 + } 1.171 + // BUG(paddy): really need to do a security audit on our cookie 1.172 + cookie := http.Cookie{ 1.173 + Name: authCookieName, 1.174 + Value: session.ID, 1.175 + Expires: time.Now().Add(24 * 7 * time.Hour), 1.176 + HttpOnly: true, 1.177 + } 1.178 + http.SetCookie(w, &cookie) 1.179 + redirectTo := r.URL.Query().Get("from") 1.180 + if redirectTo == "" { 1.181 + redirectTo = "/" 1.182 + } 1.183 + http.Redirect(w, r, redirectTo, http.StatusFound) 1.184 + return 1.185 + } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked { 1.186 + w.WriteHeader(http.StatusInternalServerError) 1.187 + w.Write([]byte(err.Error())) 1.188 + return 1.189 + } else { 1.190 + errors = append(errors, err) 1.191 + } 1.192 + } 1.193 + context.Render(w, loginTemplateName, map[string]interface{}{ 1.194 + "errors": errors, 1.195 + }) 1.196 +}