auth

Paddy 2015-03-24 Parent:026adb0c7fc4 Child:cf6c1f05eb21

153:3e8964a914ef Go to Latest

auth/session.go

Fix tests for scopeStore. Update all our tests to use the PG_TEST_DB environment variable if set, and use that to control whether or not the postgres tests get run. testing.Short() just wasn't working. Update ErrScopeNotFound and ErrScopeAlreadyExists to be variables instead of types, because PostgreSQL (annoyingly) offers no way to determine which specific row insertion caused the problem, and I anticipate this being a problem that is ongoing. So it's really just not worth it. Stop getScopes from returning an ErrScopeNotFound. Let's return what we find, and let the absence of what we didn't find speak for itself. Fix an error with generating the SQL for the postgres.createScopes call. We used to generate it in a way that was invalid (not joining values with ",") when more than one set of values was supplied. Hooray, testing! Update the postgres scopeStore to return ErrScopeNotFound and ErrScopeAlreadyExists errors, as appropriate. Update our tests to reflect that ErrScopeNotFound and ErrScopeAlreadyExists are now variables, not types.

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@70 94 removeSession(id string) error
paddy@70 95 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
paddy@70 96 }
paddy@77 97
paddy@77 98 func (m *memstore) createSession(session Session) error {
paddy@77 99 m.sessionLock.Lock()
paddy@77 100 defer m.sessionLock.Unlock()
paddy@77 101 if _, ok := m.sessions[session.ID]; ok {
paddy@77 102 return ErrSessionAlreadyExists
paddy@77 103 }
paddy@77 104 m.sessions[session.ID] = session
paddy@77 105 return nil
paddy@77 106 }
paddy@77 107
paddy@77 108 func (m *memstore) getSession(id string) (Session, error) {
paddy@77 109 m.sessionLock.RLock()
paddy@77 110 defer m.sessionLock.RUnlock()
paddy@77 111 if _, ok := m.sessions[id]; !ok {
paddy@77 112 return Session{}, ErrSessionNotFound
paddy@77 113 }
paddy@77 114 return m.sessions[id], nil
paddy@77 115 }
paddy@77 116
paddy@77 117 func (m *memstore) removeSession(id string) error {
paddy@77 118 m.sessionLock.Lock()
paddy@77 119 defer m.sessionLock.Unlock()
paddy@77 120 if _, ok := m.sessions[id]; !ok {
paddy@77 121 return ErrSessionNotFound
paddy@77 122 }
paddy@77 123 delete(m.sessions, id)
paddy@77 124 return nil
paddy@77 125 }
paddy@77 126
paddy@77 127 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
paddy@77 128 m.sessionLock.RLock()
paddy@77 129 defer m.sessionLock.RUnlock()
paddy@77 130 res := []Session{}
paddy@77 131 for _, session := range m.sessions {
paddy@77 132 if int64(len(res)) >= num {
paddy@77 133 break
paddy@77 134 }
paddy@77 135 if profile != nil && !profile.Equal(session.ProfileID) {
paddy@77 136 continue
paddy@77 137 }
paddy@77 138 if !before.IsZero() && session.Created.After(before) {
paddy@77 139 continue
paddy@77 140 }
paddy@77 141 res = append(res, session)
paddy@77 142 }
paddy@89 143 sorted := sortedSessions(res)
paddy@89 144 sort.Sort(sorted)
paddy@89 145 res = []Session(sorted)
paddy@77 146 return res, nil
paddy@77 147 }
paddy@98 148
paddy@98 149 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
paddy@98 150 func RegisterSessionHandlers(r *mux.Router, context Context) {
paddy@98 151 r.Handle("/login", wrap(context, CreateSessionHandler))
paddy@128 152 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
paddy@128 153 // BUG(paddy): We need to implement a handler for terminating sessions.
paddy@98 154 }
paddy@98 155
paddy@132 156 func checkCSRF(r *http.Request, s Session) error {
paddy@132 157 if r.PostFormValue("csrftoken") != s.CSRFToken {
paddy@132 158 return ErrCSRFAttempt
paddy@132 159 }
paddy@132 160 return nil
paddy@132 161 }
paddy@132 162
paddy@98 163 func checkCookie(r *http.Request, context Context) (Session, error) {
paddy@98 164 cookie, err := r.Cookie(authCookieName)
paddy@98 165 if err == http.ErrNoCookie {
paddy@98 166 return Session{}, ErrNoSession
paddy@98 167 } else if err != nil {
paddy@98 168 log.Println(err)
paddy@98 169 return Session{}, err
paddy@98 170 }
paddy@98 171 sess, err := context.GetSession(cookie.Value)
paddy@98 172 if err == ErrSessionNotFound {
paddy@98 173 return Session{}, ErrInvalidSession
paddy@98 174 } else if err != nil {
paddy@98 175 return Session{}, err
paddy@98 176 }
paddy@98 177 if !sess.Active {
paddy@98 178 return Session{}, ErrInvalidSession
paddy@98 179 }
paddy@132 180 if time.Now().After(sess.Expires) {
paddy@132 181 return Session{}, ErrInvalidSession
paddy@132 182 }
paddy@98 183 return sess, nil
paddy@98 184 }
paddy@98 185
paddy@98 186 func buildLoginRedirect(r *http.Request, context Context) string {
paddy@98 187 if context.loginURI == nil {
paddy@98 188 return ""
paddy@98 189 }
paddy@98 190 uri := *context.loginURI
paddy@98 191 q := uri.Query()
paddy@98 192 q.Set("from", r.URL.String())
paddy@98 193 uri.RawQuery = q.Encode()
paddy@98 194 return uri.String()
paddy@98 195 }
paddy@98 196
paddy@98 197 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
paddy@98 198 realPass, err := hex.DecodeString(profile.Passphrase)
paddy@98 199 if err != nil {
paddy@98 200 return false, err
paddy@98 201 }
paddy@103 202 realSalt, err := hex.DecodeString(profile.Salt)
paddy@103 203 if err != nil {
paddy@103 204 return false, err
paddy@103 205 }
paddy@103 206 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
paddy@98 207 if !pass.Compare(candidate, realPass) {
paddy@98 208 return false, ErrIncorrectAuth
paddy@98 209 }
paddy@98 210 return true, nil
paddy@98 211 }
paddy@98 212
paddy@103 213 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
paddy@103 214 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
paddy@103 215 if err != nil {
paddy@103 216 return "", "", err
paddy@103 217 }
paddy@103 218 result = hex.EncodeToString(passBytes)
paddy@103 219 salt = hex.EncodeToString(saltBytes)
paddy@103 220 return result, salt, err
paddy@98 221 }
paddy@98 222
paddy@98 223 func pbkdf2sha256calc() (int, error) {
paddy@98 224 return pass.CalculateIterations(sha256.New)
paddy@98 225 }
paddy@98 226
paddy@139 227 func isAuthError(err error) bool {
paddy@139 228 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
paddy@139 229 }
paddy@139 230
paddy@98 231 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@98 232 profile, err := context.GetProfileByLogin(user)
paddy@98 233 if err != nil {
paddy@98 234 if err == ErrProfileNotFound || err == ErrLoginNotFound {
paddy@98 235 return Profile{}, ErrIncorrectAuth
paddy@98 236 }
paddy@98 237 return Profile{}, err
paddy@98 238 }
paddy@98 239 if profile.Compromised {
paddy@98 240 return Profile{}, ErrProfileCompromised
paddy@98 241 }
paddy@98 242 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
paddy@98 243 return profile, ErrProfileLocked
paddy@98 244 }
paddy@98 245 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
paddy@98 246 if !ok {
paddy@98 247 return Profile{}, ErrInvalidPassphraseScheme
paddy@98 248 }
paddy@98 249 result, err := scheme.check(profile, passphrase)
paddy@98 250 if !result {
paddy@98 251 return Profile{}, err
paddy@98 252 }
paddy@98 253 return profile, nil
paddy@98 254 }
paddy@98 255
paddy@98 256 // CreateSessionHandler allows the user to log into their account and create their session.
paddy@98 257 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@98 258 errors := []error{}
paddy@98 259 if r.Method == "POST" {
paddy@98 260 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
paddy@98 261 if err == nil {
paddy@98 262 ip := r.Header.Get("X-Forwarded-For")
paddy@98 263 if ip == "" {
paddy@98 264 ip = r.RemoteAddr
paddy@98 265 }
paddy@132 266 sessionID := make([]byte, 32)
paddy@132 267 csrfToken := make([]byte, 32)
paddy@132 268 _, err = rand.Read(sessionID)
paddy@132 269 if err != nil {
paddy@132 270 log.Println("Error reading CSPRNG for session ID:", err)
paddy@132 271 w.WriteHeader(http.StatusInternalServerError)
paddy@132 272 w.Write([]byte("Internal error"))
paddy@132 273 return
paddy@132 274 }
paddy@132 275 _, err = rand.Read(csrfToken)
paddy@132 276 if err != nil {
paddy@132 277 log.Println("Error reading CSPRNG for CSRF token:", err)
paddy@132 278 w.WriteHeader(http.StatusInternalServerError)
paddy@132 279 w.Write([]byte("internal error"))
paddy@132 280 return
paddy@132 281 }
paddy@98 282 session := Session{
paddy@132 283 ID: base64.StdEncoding.EncodeToString(sessionID),
paddy@98 284 IP: ip,
paddy@98 285 UserAgent: r.UserAgent(),
paddy@98 286 ProfileID: profile.ID,
paddy@98 287 Login: r.PostFormValue("login"),
paddy@98 288 Created: time.Now(),
paddy@132 289 Expires: time.Now().Add(time.Hour),
paddy@98 290 Active: true,
paddy@132 291 CSRFToken: base64.StdEncoding.EncodeToString(csrfToken),
paddy@98 292 }
paddy@98 293 err = context.CreateSession(session)
paddy@98 294 if err != nil {
paddy@98 295 w.WriteHeader(http.StatusInternalServerError)
paddy@98 296 w.Write([]byte(err.Error()))
paddy@98 297 return
paddy@98 298 }
paddy@132 299 // BUG(paddy): We really need to do a security audit on our cookies.
paddy@98 300 cookie := http.Cookie{
paddy@98 301 Name: authCookieName,
paddy@98 302 Value: session.ID,
paddy@132 303 Expires: session.Expires,
paddy@98 304 HttpOnly: true,
paddy@132 305 Secure: context.config.secureCookie,
paddy@98 306 }
paddy@98 307 http.SetCookie(w, &cookie)
paddy@98 308 redirectTo := r.URL.Query().Get("from")
paddy@98 309 if redirectTo == "" {
paddy@98 310 redirectTo = "/"
paddy@98 311 }
paddy@98 312 http.Redirect(w, r, redirectTo, http.StatusFound)
paddy@98 313 return
paddy@139 314 } else if !isAuthError(err) {
paddy@98 315 w.WriteHeader(http.StatusInternalServerError)
paddy@98 316 w.Write([]byte(err.Error()))
paddy@98 317 return
paddy@98 318 } else {
paddy@98 319 errors = append(errors, err)
paddy@98 320 }
paddy@98 321 }
paddy@98 322 context.Render(w, loginTemplateName, map[string]interface{}{
paddy@98 323 "errors": errors,
paddy@98 324 })
paddy@98 325 }
paddy@119 326
paddy@135 327 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
paddy@119 328 enc := json.NewEncoder(w)
paddy@119 329 username := r.PostFormValue("username")
paddy@119 330 password := r.PostFormValue("password")
paddy@135 331 scopes = strings.Split(r.PostFormValue("scope"), " ")
paddy@119 332 profile, err := authenticate(username, password, context)
paddy@119 333 if err != nil {
paddy@139 334 if isAuthError(err) {
paddy@119 335 w.WriteHeader(http.StatusBadRequest)
paddy@119 336 renderJSONError(enc, "invalid_grant")
paddy@119 337 return
paddy@119 338 }
paddy@119 339 w.WriteHeader(http.StatusInternalServerError)
paddy@119 340 w.Write([]byte(err.Error()))
paddy@119 341 return
paddy@119 342 }
paddy@119 343 profileID = profile.ID
paddy@119 344 valid = true
paddy@119 345 return
paddy@119 346 }
paddy@124 347
paddy@124 348 func credentialsAuditString(r *http.Request) string {
paddy@124 349 return "credentials"
paddy@124 350 }