auth

Paddy 2015-12-14 Parent:b7e685839a1b

182:cd5f07f9811b Go to Latest

auth/session.go

Update nsq import path. go-nsq has moved to nsqio/go-nsq, so we need to update the import path appropriately.

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 }