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
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 }