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.
| paddy@70 | 1 package auth |
| paddy@70 | 2 |
| paddy@70 | 3 import ( |
| paddy@98 | 4 "crypto/sha256" |
| paddy@98 | 5 "encoding/hex" |
| paddy@70 | 6 "errors" |
| paddy@98 | 7 "log" |
| paddy@98 | 8 "net/http" |
| paddy@89 | 9 "sort" |
| paddy@70 | 10 "time" |
| paddy@70 | 11 |
| paddy@98 | 12 "code.secondbit.org/pass" |
| paddy@70 | 13 "code.secondbit.org/uuid" |
| paddy@98 | 14 |
| paddy@98 | 15 "github.com/gorilla/mux" |
| paddy@98 | 16 ) |
| paddy@98 | 17 |
| paddy@98 | 18 const ( |
| paddy@98 | 19 loginTemplateName = "login" |
| paddy@70 | 20 ) |
| paddy@70 | 21 |
| paddy@70 | 22 var ( |
| paddy@70 | 23 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first. |
| paddy@70 | 24 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context") |
| paddy@70 | 25 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore. |
| paddy@70 | 26 ErrSessionNotFound = errors.New("session not found in sessionStore") |
| paddy@70 | 27 // ErrInvalidSession is returned when a Session is specified but is not valid. |
| paddy@70 | 28 ErrInvalidSession = errors.New("session is not valid") |
| paddy@77 | 29 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore. |
| paddy@77 | 30 ErrSessionAlreadyExists = errors.New("session already exists") |
| paddy@98 | 31 |
| paddy@98 | 32 passphraseSchemes = map[int]passphraseScheme{ |
| paddy@98 | 33 1: { |
| paddy@98 | 34 check: pbkdf2sha256check, |
| paddy@98 | 35 create: pbkdf2sha256create, |
| paddy@98 | 36 calculateIterations: pbkdf2sha256calc, |
| paddy@98 | 37 }, |
| paddy@98 | 38 } |
| paddy@70 | 39 ) |
| paddy@70 | 40 |
| paddy@98 | 41 type passphraseScheme struct { |
| paddy@98 | 42 check func(profile Profile, passphrase string) (bool, error) |
| paddy@98 | 43 create func(passphrase string, iterations int) (result, salt []byte, err error) |
| paddy@98 | 44 calculateIterations func() (int, error) |
| paddy@98 | 45 } |
| paddy@98 | 46 |
| paddy@70 | 47 // Session represents a user's authenticated session, associating it with a profile |
| paddy@70 | 48 // and some audit data. |
| paddy@70 | 49 type Session struct { |
| paddy@70 | 50 ID string |
| paddy@70 | 51 IP string |
| paddy@70 | 52 UserAgent string |
| paddy@70 | 53 ProfileID uuid.ID |
| paddy@98 | 54 Login string |
| paddy@70 | 55 Created time.Time |
| paddy@70 | 56 Active bool |
| paddy@70 | 57 } |
| paddy@70 | 58 |
| paddy@89 | 59 type sortedSessions []Session |
| paddy@89 | 60 |
| paddy@89 | 61 func (s sortedSessions) Len() int { |
| paddy@89 | 62 return len(s) |
| paddy@89 | 63 } |
| paddy@89 | 64 |
| paddy@89 | 65 func (s sortedSessions) Less(i, j int) bool { |
| paddy@89 | 66 return s[i].Created.After(s[j].Created) |
| paddy@89 | 67 } |
| paddy@89 | 68 |
| paddy@89 | 69 func (s sortedSessions) Swap(i, j int) { |
| paddy@89 | 70 s[i], s[j] = s[j], s[i] |
| paddy@89 | 71 } |
| paddy@89 | 72 |
| paddy@70 | 73 type sessionStore interface { |
| paddy@70 | 74 createSession(session Session) error |
| paddy@70 | 75 getSession(id string) (Session, error) |
| paddy@70 | 76 removeSession(id string) error |
| paddy@70 | 77 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) |
| paddy@70 | 78 } |
| paddy@77 | 79 |
| paddy@77 | 80 func (m *memstore) createSession(session Session) error { |
| paddy@77 | 81 m.sessionLock.Lock() |
| paddy@77 | 82 defer m.sessionLock.Unlock() |
| paddy@77 | 83 if _, ok := m.sessions[session.ID]; ok { |
| paddy@77 | 84 return ErrSessionAlreadyExists |
| paddy@77 | 85 } |
| paddy@77 | 86 m.sessions[session.ID] = session |
| paddy@77 | 87 return nil |
| paddy@77 | 88 } |
| paddy@77 | 89 |
| paddy@77 | 90 func (m *memstore) getSession(id string) (Session, error) { |
| paddy@77 | 91 m.sessionLock.RLock() |
| paddy@77 | 92 defer m.sessionLock.RUnlock() |
| paddy@77 | 93 if _, ok := m.sessions[id]; !ok { |
| paddy@77 | 94 return Session{}, ErrSessionNotFound |
| paddy@77 | 95 } |
| paddy@77 | 96 return m.sessions[id], nil |
| paddy@77 | 97 } |
| paddy@77 | 98 |
| paddy@77 | 99 func (m *memstore) removeSession(id string) error { |
| paddy@77 | 100 m.sessionLock.Lock() |
| paddy@77 | 101 defer m.sessionLock.Unlock() |
| paddy@77 | 102 if _, ok := m.sessions[id]; !ok { |
| paddy@77 | 103 return ErrSessionNotFound |
| paddy@77 | 104 } |
| paddy@77 | 105 delete(m.sessions, id) |
| paddy@77 | 106 return nil |
| paddy@77 | 107 } |
| paddy@77 | 108 |
| paddy@77 | 109 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) { |
| paddy@77 | 110 m.sessionLock.RLock() |
| paddy@77 | 111 defer m.sessionLock.RUnlock() |
| paddy@77 | 112 res := []Session{} |
| paddy@77 | 113 for _, session := range m.sessions { |
| paddy@77 | 114 if int64(len(res)) >= num { |
| paddy@77 | 115 break |
| paddy@77 | 116 } |
| paddy@77 | 117 if profile != nil && !profile.Equal(session.ProfileID) { |
| paddy@77 | 118 continue |
| paddy@77 | 119 } |
| paddy@77 | 120 if !before.IsZero() && session.Created.After(before) { |
| paddy@77 | 121 continue |
| paddy@77 | 122 } |
| paddy@77 | 123 res = append(res, session) |
| paddy@77 | 124 } |
| paddy@89 | 125 sorted := sortedSessions(res) |
| paddy@89 | 126 sort.Sort(sorted) |
| paddy@89 | 127 res = []Session(sorted) |
| paddy@77 | 128 return res, nil |
| paddy@77 | 129 } |
| paddy@98 | 130 |
| paddy@98 | 131 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout. |
| paddy@98 | 132 func RegisterSessionHandlers(r *mux.Router, context Context) { |
| paddy@98 | 133 r.Handle("/login", wrap(context, CreateSessionHandler)) |
| paddy@98 | 134 } |
| paddy@98 | 135 |
| paddy@98 | 136 func checkCookie(r *http.Request, context Context) (Session, error) { |
| paddy@98 | 137 cookie, err := r.Cookie(authCookieName) |
| paddy@98 | 138 if err == http.ErrNoCookie { |
| paddy@98 | 139 return Session{}, ErrNoSession |
| paddy@98 | 140 } else if err != nil { |
| paddy@98 | 141 log.Println(err) |
| paddy@98 | 142 return Session{}, err |
| paddy@98 | 143 } |
| paddy@98 | 144 sess, err := context.GetSession(cookie.Value) |
| paddy@98 | 145 if err == ErrSessionNotFound { |
| paddy@98 | 146 return Session{}, ErrInvalidSession |
| paddy@98 | 147 } else if err != nil { |
| paddy@98 | 148 return Session{}, err |
| paddy@98 | 149 } |
| paddy@98 | 150 if !sess.Active { |
| paddy@98 | 151 return Session{}, ErrInvalidSession |
| paddy@98 | 152 } |
| paddy@98 | 153 return sess, nil |
| paddy@98 | 154 } |
| paddy@98 | 155 |
| paddy@98 | 156 func buildLoginRedirect(r *http.Request, context Context) string { |
| paddy@98 | 157 if context.loginURI == nil { |
| paddy@98 | 158 return "" |
| paddy@98 | 159 } |
| paddy@98 | 160 uri := *context.loginURI |
| paddy@98 | 161 q := uri.Query() |
| paddy@98 | 162 q.Set("from", r.URL.String()) |
| paddy@98 | 163 uri.RawQuery = q.Encode() |
| paddy@98 | 164 return uri.String() |
| paddy@98 | 165 } |
| paddy@98 | 166 |
| paddy@98 | 167 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) { |
| paddy@98 | 168 realPass, err := hex.DecodeString(profile.Passphrase) |
| paddy@98 | 169 if err != nil { |
| paddy@98 | 170 return false, err |
| paddy@98 | 171 } |
| paddy@98 | 172 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt)) |
| paddy@98 | 173 if !pass.Compare(candidate, realPass) { |
| paddy@98 | 174 return false, ErrIncorrectAuth |
| paddy@98 | 175 } |
| paddy@98 | 176 return true, nil |
| paddy@98 | 177 } |
| paddy@98 | 178 |
| paddy@98 | 179 func pbkdf2sha256create(passphrase string, iters int) (result, salt []byte, err error) { |
| paddy@98 | 180 return pass.Create(sha256.New, iters, []byte(passphrase)) |
| paddy@98 | 181 } |
| paddy@98 | 182 |
| paddy@98 | 183 func pbkdf2sha256calc() (int, error) { |
| paddy@98 | 184 return pass.CalculateIterations(sha256.New) |
| paddy@98 | 185 } |
| paddy@98 | 186 |
| paddy@98 | 187 func authenticate(user, passphrase string, context Context) (Profile, error) { |
| paddy@98 | 188 profile, err := context.GetProfileByLogin(user) |
| paddy@98 | 189 if err != nil { |
| paddy@98 | 190 if err == ErrProfileNotFound || err == ErrLoginNotFound { |
| paddy@98 | 191 return Profile{}, ErrIncorrectAuth |
| paddy@98 | 192 } |
| paddy@98 | 193 return Profile{}, err |
| paddy@98 | 194 } |
| paddy@98 | 195 if profile.Compromised { |
| paddy@98 | 196 return Profile{}, ErrProfileCompromised |
| paddy@98 | 197 } |
| paddy@98 | 198 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) { |
| paddy@98 | 199 return profile, ErrProfileLocked |
| paddy@98 | 200 } |
| paddy@98 | 201 scheme, ok := passphraseSchemes[profile.PassphraseScheme] |
| paddy@98 | 202 if !ok { |
| paddy@98 | 203 return Profile{}, ErrInvalidPassphraseScheme |
| paddy@98 | 204 } |
| paddy@98 | 205 result, err := scheme.check(profile, passphrase) |
| paddy@98 | 206 if !result { |
| paddy@98 | 207 return Profile{}, err |
| paddy@98 | 208 } |
| paddy@98 | 209 return profile, nil |
| paddy@98 | 210 } |
| paddy@98 | 211 |
| paddy@98 | 212 // CreateSessionHandler allows the user to log into their account and create their session. |
| paddy@98 | 213 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@98 | 214 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit |
| paddy@98 | 215 errors := []error{} |
| paddy@98 | 216 if r.Method == "POST" { |
| paddy@98 | 217 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context) |
| paddy@98 | 218 if err == nil { |
| paddy@98 | 219 ip := r.Header.Get("X-Forwarded-For") |
| paddy@98 | 220 if ip == "" { |
| paddy@98 | 221 ip = r.RemoteAddr |
| paddy@98 | 222 } |
| paddy@98 | 223 session := Session{ |
| paddy@98 | 224 ID: uuid.NewID().String(), |
| paddy@98 | 225 IP: ip, |
| paddy@98 | 226 UserAgent: r.UserAgent(), |
| paddy@98 | 227 ProfileID: profile.ID, |
| paddy@98 | 228 Login: r.PostFormValue("login"), |
| paddy@98 | 229 Created: time.Now(), |
| paddy@98 | 230 Active: true, |
| paddy@98 | 231 } |
| paddy@98 | 232 err = context.CreateSession(session) |
| paddy@98 | 233 if err != nil { |
| paddy@98 | 234 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 235 w.Write([]byte(err.Error())) |
| paddy@98 | 236 return |
| paddy@98 | 237 } |
| paddy@98 | 238 // BUG(paddy): really need to do a security audit on our cookie |
| paddy@98 | 239 cookie := http.Cookie{ |
| paddy@98 | 240 Name: authCookieName, |
| paddy@98 | 241 Value: session.ID, |
| paddy@98 | 242 Expires: time.Now().Add(24 * 7 * time.Hour), |
| paddy@98 | 243 HttpOnly: true, |
| paddy@98 | 244 } |
| paddy@98 | 245 http.SetCookie(w, &cookie) |
| paddy@98 | 246 redirectTo := r.URL.Query().Get("from") |
| paddy@98 | 247 if redirectTo == "" { |
| paddy@98 | 248 redirectTo = "/" |
| paddy@98 | 249 } |
| paddy@98 | 250 http.Redirect(w, r, redirectTo, http.StatusFound) |
| paddy@98 | 251 return |
| paddy@98 | 252 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked { |
| paddy@98 | 253 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 254 w.Write([]byte(err.Error())) |
| paddy@98 | 255 return |
| paddy@98 | 256 } else { |
| paddy@98 | 257 errors = append(errors, err) |
| paddy@98 | 258 } |
| paddy@98 | 259 } |
| paddy@98 | 260 context.Render(w, loginTemplateName, map[string]interface{}{ |
| paddy@98 | 261 "errors": errors, |
| paddy@98 | 262 }) |
| paddy@98 | 263 } |