auth

Paddy 2015-12-14 Parent:8ecb60d29b0d

181:b7e685839a1b Go to Latest

auth/session.go

Break out scopes and events. This repo has gotten unwieldy, and there are portions of it that need to be imported by a large number of other packages. For example, scopes will be used in almost every API we write. Rather than importing the entirety of this codebase into every API we write, I've opted to move the scope logic out into a scopes package, with a subpackage for the defined types, which is all most projects actually want to import. We also define some event type constants, and importing those shouldn't require a project to import all our dependencies, either. So I made an events subpackage that just holds those constants. This package has become a little bit of a red-headed stepchild and is do for a refactor, but I'm trying to put that off as long as I can. The refactoring of our scopes stuff has left a bug wherein a token can be granted for scopes that don't exist. I'm going to need to revisit that, and also how to limit scopes to only be granted to the users that should be able to request them. But that's a battle for another day.

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