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