auth

Paddy 2015-04-11 Parent:cf6c1f05eb21 Child:6f473576c6ae

161:849f3820b164 Go to Latest

auth/session.go

Stop soft-deleting Profiles and actually delete them. The information we're storing in Profiles isn't unique enough that we should go through the hassle we're going through to soft-delete it. Add a deleteProfile method to our profileStore, and implement it for our postgres and memstore implementations. Add a DeleteProfile wrapper for our Context. Remove the Deleted property from the Profile type and the ProfileChange type, and update references to it. Stop cleaning up after our Profile in the UpdateProfileHandler, because there's no longer any way to delete the Profile from the UpdateProfileHandler. Update our get/list* methods so they don't filter on the non-existent Deleted property anymore. Update our SQL schema definition to not include the deleted column. Update our profile tests to use the DeleteProfile method and stop comparing the no-longer-existing Deleted property.

History
1 package auth
3 import (
4 "crypto/rand"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "log"
11 "net/http"
12 "sort"
13 "strings"
14 "time"
16 "code.secondbit.org/pass.hg"
17 "code.secondbit.org/uuid.hg"
18 "github.com/gorilla/mux"
19 )
21 const (
22 authCookieName = "auth"
23 loginTemplateName = "login"
24 )
26 func init() {
27 RegisterGrantType("password", GrantType{
28 Validate: credentialsValidate,
29 Invalidate: nil,
30 IssuesRefresh: true,
31 ReturnToken: RenderJSONToken,
32 AuditString: credentialsAuditString,
33 })
34 }
36 var (
37 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
38 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
39 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
40 ErrSessionNotFound = errors.New("session not found in sessionStore")
41 // ErrInvalidSession is returned when a Session is specified but is not valid.
42 ErrInvalidSession = errors.New("session is not valid")
43 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
44 ErrSessionAlreadyExists = errors.New("session already exists")
45 // ErrCSRFAttempt is returned when a CSRF attempt is detected.
46 ErrCSRFAttempt = errors.New("CSRF attempt")
48 passphraseSchemes = map[int]passphraseScheme{
49 1: {
50 check: pbkdf2sha256check,
51 create: pbkdf2sha256create,
52 calculateIterations: pbkdf2sha256calc,
53 },
54 }
55 )
57 type passphraseScheme struct {
58 check func(profile Profile, passphrase string) (bool, error)
59 create func(passphrase string, iterations int) (result, salt string, err error)
60 calculateIterations func() (int, error)
61 }
63 // Session represents a user's authenticated session, associating it with a profile
64 // and some audit data.
65 type Session struct {
66 ID string
67 IP string
68 UserAgent string
69 ProfileID uuid.ID
70 Login string
71 Created time.Time
72 Expires time.Time
73 Active bool
74 CSRFToken string
75 }
77 type sortedSessions []Session
79 func (s sortedSessions) Len() int {
80 return len(s)
81 }
83 func (s sortedSessions) Less(i, j int) bool {
84 return s[i].Created.After(s[j].Created)
85 }
87 func (s sortedSessions) Swap(i, j int) {
88 s[i], s[j] = s[j], s[i]
89 }
91 type sessionStore interface {
92 createSession(session Session) error
93 getSession(id string) (Session, error)
94 terminateSession(id string) error
95 removeSession(id string) error
96 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
97 }
99 func (m *memstore) createSession(session Session) error {
100 m.sessionLock.Lock()
101 defer m.sessionLock.Unlock()
102 if _, ok := m.sessions[session.ID]; ok {
103 return ErrSessionAlreadyExists
104 }
105 m.sessions[session.ID] = session
106 return nil
107 }
109 func (m *memstore) getSession(id string) (Session, error) {
110 m.sessionLock.RLock()
111 defer m.sessionLock.RUnlock()
112 if _, ok := m.sessions[id]; !ok {
113 return Session{}, ErrSessionNotFound
114 }
115 return m.sessions[id], nil
116 }
118 func (m *memstore) terminateSession(id string) error {
119 m.sessionLock.RLock()
120 defer m.sessionLock.RUnlock()
121 sess, ok := m.sessions[id]
122 if !ok {
123 return ErrSessionNotFound
124 }
125 sess.Active = false
126 m.sessions[id] = sess
127 return nil
128 }
130 func (m *memstore) removeSession(id string) error {
131 m.sessionLock.Lock()
132 defer m.sessionLock.Unlock()
133 if _, ok := m.sessions[id]; !ok {
134 return ErrSessionNotFound
135 }
136 delete(m.sessions, id)
137 return nil
138 }
140 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
141 m.sessionLock.RLock()
142 defer m.sessionLock.RUnlock()
143 res := []Session{}
144 for _, session := range m.sessions {
145 if int64(len(res)) >= num {
146 break
147 }
148 if profile != nil && !profile.Equal(session.ProfileID) {
149 continue
150 }
151 if !before.IsZero() && session.Created.After(before) {
152 continue
153 }
154 res = append(res, session)
155 }
156 sorted := sortedSessions(res)
157 sort.Sort(sorted)
158 res = []Session(sorted)
159 return res, nil
160 }
162 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
163 func RegisterSessionHandlers(r *mux.Router, context Context) {
164 r.Handle("/login", wrap(context, CreateSessionHandler))
165 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
166 r.Handle("/sessions/{id}", wrap(context, TerminateSessionHandler)).Methods("OPTIONS", "DELETE")
167 }
169 func checkCSRF(r *http.Request, s Session) error {
170 if r.PostFormValue("csrftoken") != s.CSRFToken {
171 return ErrCSRFAttempt
172 }
173 return nil
174 }
176 func checkCookie(r *http.Request, context Context) (Session, error) {
177 cookie, err := r.Cookie(authCookieName)
178 if err == http.ErrNoCookie {
179 return Session{}, ErrNoSession
180 } else if err != nil {
181 log.Println(err)
182 return Session{}, err
183 }
184 sess, err := context.GetSession(cookie.Value)
185 if err == ErrSessionNotFound {
186 return Session{}, ErrInvalidSession
187 } else if err != nil {
188 return Session{}, err
189 }
190 if !sess.Active {
191 return Session{}, ErrInvalidSession
192 }
193 if time.Now().After(sess.Expires) {
194 return Session{}, ErrInvalidSession
195 }
196 return sess, nil
197 }
199 func buildLoginRedirect(r *http.Request, context Context) string {
200 if context.loginURI == nil {
201 return ""
202 }
203 uri := *context.loginURI
204 q := uri.Query()
205 q.Set("from", r.URL.String())
206 uri.RawQuery = q.Encode()
207 return uri.String()
208 }
210 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
211 realPass, err := hex.DecodeString(profile.Passphrase)
212 if err != nil {
213 return false, err
214 }
215 realSalt, err := hex.DecodeString(profile.Salt)
216 if err != nil {
217 return false, err
218 }
219 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
220 if !pass.Compare(candidate, realPass) {
221 return false, ErrIncorrectAuth
222 }
223 return true, nil
224 }
226 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
227 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
228 if err != nil {
229 return "", "", err
230 }
231 result = hex.EncodeToString(passBytes)
232 salt = hex.EncodeToString(saltBytes)
233 return result, salt, err
234 }
236 func pbkdf2sha256calc() (int, error) {
237 return pass.CalculateIterations(sha256.New)
238 }
240 func isAuthError(err error) bool {
241 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
242 }
244 func authenticate(user, passphrase string, context Context) (Profile, error) {
245 profile, err := context.GetProfileByLogin(user)
246 if err != nil {
247 if err == ErrProfileNotFound || err == ErrLoginNotFound {
248 return Profile{}, ErrIncorrectAuth
249 }
250 return Profile{}, err
251 }
252 if profile.Compromised {
253 return Profile{}, ErrProfileCompromised
254 }
255 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
256 return profile, ErrProfileLocked
257 }
258 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
259 if !ok {
260 return Profile{}, ErrInvalidPassphraseScheme
261 }
262 result, err := scheme.check(profile, passphrase)
263 if !result {
264 return Profile{}, err
265 }
266 return profile, nil
267 }
269 // CreateSessionHandler allows the user to log into their account and create their session.
270 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
271 errors := []error{}
272 if r.Method == "POST" {
273 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
274 if err == nil {
275 ip := r.Header.Get("X-Forwarded-For")
276 if ip == "" {
277 ip = r.RemoteAddr
278 }
279 sessionID := make([]byte, 32)
280 csrfToken := make([]byte, 32)
281 _, err = rand.Read(sessionID)
282 if err != nil {
283 log.Println("Error reading CSPRNG for session ID:", err)
284 w.WriteHeader(http.StatusInternalServerError)
285 w.Write([]byte("Internal error"))
286 return
287 }
288 _, err = rand.Read(csrfToken)
289 if err != nil {
290 log.Println("Error reading CSPRNG for CSRF token:", err)
291 w.WriteHeader(http.StatusInternalServerError)
292 w.Write([]byte("internal error"))
293 return
294 }
295 session := Session{
296 ID: base64.URLEncoding.EncodeToString(sessionID),
297 IP: ip,
298 UserAgent: r.UserAgent(),
299 ProfileID: profile.ID,
300 Login: r.PostFormValue("login"),
301 Created: time.Now(),
302 Expires: time.Now().Add(time.Hour),
303 Active: true,
304 CSRFToken: base64.URLEncoding.EncodeToString(csrfToken),
305 }
306 err = context.CreateSession(session)
307 if err != nil {
308 w.WriteHeader(http.StatusInternalServerError)
309 w.Write([]byte(err.Error()))
310 return
311 }
312 // BUG(paddy): We really need to do a security audit on our cookies.
313 cookie := http.Cookie{
314 Name: authCookieName,
315 Value: session.ID,
316 Expires: session.Expires,
317 HttpOnly: true,
318 Secure: context.config.secureCookie,
319 }
320 http.SetCookie(w, &cookie)
321 redirectTo := r.URL.Query().Get("from")
322 if redirectTo == "" {
323 redirectTo = "/"
324 }
325 http.Redirect(w, r, redirectTo, http.StatusFound)
326 return
327 } else if !isAuthError(err) {
328 w.WriteHeader(http.StatusInternalServerError)
329 w.Write([]byte(err.Error()))
330 return
331 } else {
332 errors = append(errors, err)
333 }
334 }
335 context.Render(w, loginTemplateName, map[string]interface{}{
336 "errors": errors,
337 })
338 }
340 // TerminateSessionHandler allows the user to end their session before it expires.
341 func TerminateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
342 var errors []requestError
343 vars := mux.Vars(r)
344 if vars["id"] == "" {
345 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
346 encode(w, r, http.StatusBadRequest, response{Errors: errors})
347 return
348 }
349 id := vars["id"]
350 un, pw, ok := r.BasicAuth()
351 if !ok {
352 errors = append(errors, requestError{Slug: requestErrAccessDenied})
353 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
354 return
355 }
356 profile, err := authenticate(un, pw, context)
357 if err != nil {
358 if isAuthError(err) {
359 errors = append(errors, requestError{Slug: requestErrAccessDenied})
360 encode(w, r, http.StatusForbidden, response{Errors: errors})
361 return
362 }
363 errors = append(errors, requestError{Slug: requestErrActOfGod})
364 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
365 return
366 }
367 session, err := context.GetSession(id)
368 if err != nil {
369 if err == ErrSessionNotFound {
370 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
371 encode(w, r, http.StatusNotFound, response{Errors: errors})
372 return
373 }
374 errors = append(errors, requestError{Slug: requestErrActOfGod})
375 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
376 return
377 }
378 if !session.ProfileID.Equal(profile.ID) {
379 errors = append(errors, requestError{Slug: requestErrAccessDenied, Param: "id"})
380 encode(w, r, http.StatusForbidden, response{Errors: errors})
381 return
382 }
383 err = context.TerminateSession(id)
384 if err != nil {
385 if err == ErrSessionNotFound {
386 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
387 encode(w, r, http.StatusNotFound, response{Errors: errors})
388 return
389 }
390 errors = append(errors, requestError{Slug: requestErrActOfGod})
391 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
392 return
393 }
394 session.Active = false
395 encode(w, r, http.StatusOK, response{Sessions: []Session{session}, Errors: errors})
396 }
398 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
399 enc := json.NewEncoder(w)
400 username := r.PostFormValue("username")
401 password := r.PostFormValue("password")
402 scopes = strings.Split(r.PostFormValue("scope"), " ")
403 profile, err := authenticate(username, password, context)
404 if err != nil {
405 if isAuthError(err) {
406 w.WriteHeader(http.StatusBadRequest)
407 renderJSONError(enc, "invalid_grant")
408 return
409 }
410 w.WriteHeader(http.StatusInternalServerError)
411 w.Write([]byte(err.Error()))
412 return
413 }
414 profileID = profile.ID
415 valid = true
416 return
417 }
419 func credentialsAuditString(r *http.Request) string {
420 return "credentials"
421 }