auth
auth/session.go
Store salts and passphrases as hex-encoded strings. Update our passphraseScheme.create function signature to return strings. Hex encode our passphrases and salts when encrypthing them so they're easier to store safely. Decode our salt before using it to check candidate passphrases.
| 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@103 | 43 create func(passphrase string, iterations int) (result, salt string, 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@103 | 172 realSalt, err := hex.DecodeString(profile.Salt) |
| paddy@103 | 173 if err != nil { |
| paddy@103 | 174 return false, err |
| paddy@103 | 175 } |
| paddy@103 | 176 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt)) |
| paddy@98 | 177 if !pass.Compare(candidate, realPass) { |
| paddy@98 | 178 return false, ErrIncorrectAuth |
| paddy@98 | 179 } |
| paddy@98 | 180 return true, nil |
| paddy@98 | 181 } |
| paddy@98 | 182 |
| paddy@103 | 183 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) { |
| paddy@103 | 184 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase)) |
| paddy@103 | 185 if err != nil { |
| paddy@103 | 186 return "", "", err |
| paddy@103 | 187 } |
| paddy@103 | 188 result = hex.EncodeToString(passBytes) |
| paddy@103 | 189 salt = hex.EncodeToString(saltBytes) |
| paddy@103 | 190 return result, salt, err |
| paddy@98 | 191 } |
| paddy@98 | 192 |
| paddy@98 | 193 func pbkdf2sha256calc() (int, error) { |
| paddy@98 | 194 return pass.CalculateIterations(sha256.New) |
| paddy@98 | 195 } |
| paddy@98 | 196 |
| paddy@98 | 197 func authenticate(user, passphrase string, context Context) (Profile, error) { |
| paddy@98 | 198 profile, err := context.GetProfileByLogin(user) |
| paddy@98 | 199 if err != nil { |
| paddy@98 | 200 if err == ErrProfileNotFound || err == ErrLoginNotFound { |
| paddy@98 | 201 return Profile{}, ErrIncorrectAuth |
| paddy@98 | 202 } |
| paddy@98 | 203 return Profile{}, err |
| paddy@98 | 204 } |
| paddy@98 | 205 if profile.Compromised { |
| paddy@98 | 206 return Profile{}, ErrProfileCompromised |
| paddy@98 | 207 } |
| paddy@98 | 208 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) { |
| paddy@98 | 209 return profile, ErrProfileLocked |
| paddy@98 | 210 } |
| paddy@98 | 211 scheme, ok := passphraseSchemes[profile.PassphraseScheme] |
| paddy@98 | 212 if !ok { |
| paddy@98 | 213 return Profile{}, ErrInvalidPassphraseScheme |
| paddy@98 | 214 } |
| paddy@98 | 215 result, err := scheme.check(profile, passphrase) |
| paddy@98 | 216 if !result { |
| paddy@98 | 217 return Profile{}, err |
| paddy@98 | 218 } |
| paddy@98 | 219 return profile, nil |
| paddy@98 | 220 } |
| paddy@98 | 221 |
| paddy@98 | 222 // CreateSessionHandler allows the user to log into their account and create their session. |
| paddy@98 | 223 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@98 | 224 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit |
| paddy@98 | 225 errors := []error{} |
| paddy@98 | 226 if r.Method == "POST" { |
| paddy@98 | 227 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context) |
| paddy@98 | 228 if err == nil { |
| paddy@98 | 229 ip := r.Header.Get("X-Forwarded-For") |
| paddy@98 | 230 if ip == "" { |
| paddy@98 | 231 ip = r.RemoteAddr |
| paddy@98 | 232 } |
| paddy@98 | 233 session := Session{ |
| paddy@98 | 234 ID: uuid.NewID().String(), |
| paddy@98 | 235 IP: ip, |
| paddy@98 | 236 UserAgent: r.UserAgent(), |
| paddy@98 | 237 ProfileID: profile.ID, |
| paddy@98 | 238 Login: r.PostFormValue("login"), |
| paddy@98 | 239 Created: time.Now(), |
| paddy@98 | 240 Active: true, |
| paddy@98 | 241 } |
| paddy@98 | 242 err = context.CreateSession(session) |
| paddy@98 | 243 if err != nil { |
| paddy@98 | 244 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 245 w.Write([]byte(err.Error())) |
| paddy@98 | 246 return |
| paddy@98 | 247 } |
| paddy@98 | 248 // BUG(paddy): really need to do a security audit on our cookie |
| paddy@98 | 249 cookie := http.Cookie{ |
| paddy@98 | 250 Name: authCookieName, |
| paddy@98 | 251 Value: session.ID, |
| paddy@98 | 252 Expires: time.Now().Add(24 * 7 * time.Hour), |
| paddy@98 | 253 HttpOnly: true, |
| paddy@98 | 254 } |
| paddy@98 | 255 http.SetCookie(w, &cookie) |
| paddy@98 | 256 redirectTo := r.URL.Query().Get("from") |
| paddy@98 | 257 if redirectTo == "" { |
| paddy@98 | 258 redirectTo = "/" |
| paddy@98 | 259 } |
| paddy@98 | 260 http.Redirect(w, r, redirectTo, http.StatusFound) |
| paddy@98 | 261 return |
| paddy@98 | 262 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked { |
| paddy@98 | 263 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 264 w.Write([]byte(err.Error())) |
| paddy@98 | 265 return |
| paddy@98 | 266 } else { |
| paddy@98 | 267 errors = append(errors, err) |
| paddy@98 | 268 } |
| paddy@98 | 269 } |
| paddy@98 | 270 context.Render(w, loginTemplateName, map[string]interface{}{ |
| paddy@98 | 271 "errors": errors, |
| paddy@98 | 272 }) |
| paddy@98 | 273 } |