auth

Paddy 2015-05-17 Parent:73e12d5a1124 Child:8ecb60d29b0d

169:37a42585660e Go to Latest

auth/session.go

Create interfaces for login verification flow. We needed an interface that we could use to say "send the email to verify the user's login" so that we could verify the emails we have are actually valid. This implements an NSQ version that sends an email_verification event. We'll get listener implementations that pull these messages off NSQ and actually send the emails. This also implements, for testing purposes, a version that just echoes the Login Value and the Verification code to stdout.

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