auth

Paddy 2015-01-24 Parent:23c1a07c8a61 Child:163ce22fa4c9

129:4f5d13d2f7c7 Go to Latest

auth/session.go

Test our getClientAuth helper, switch to table-based tests. Our getClientAuth helper was being tested implicitly when we tested our verifyClient helper, but let's test them separately. While we're at it, let's use table based tests instead of copy and paste. I noticed a lot of copy/paste errors while I was updating this, and the less test code we have and the easier we make it to test new edge cases, the better of we are.

History
paddy@70 1 package auth
paddy@70 2
paddy@70 3 import (
paddy@98 4 "crypto/sha256"
paddy@98 5 "encoding/hex"
paddy@119 6 "encoding/json"
paddy@70 7 "errors"
paddy@98 8 "log"
paddy@98 9 "net/http"
paddy@89 10 "sort"
paddy@70 11 "time"
paddy@70 12
paddy@107 13 "code.secondbit.org/pass.hg"
paddy@107 14 "code.secondbit.org/uuid.hg"
paddy@98 15 "github.com/gorilla/mux"
paddy@98 16 )
paddy@98 17
paddy@98 18 const (
paddy@98 19 loginTemplateName = "login"
paddy@70 20 )
paddy@70 21
paddy@119 22 func init() {
paddy@119 23 RegisterGrantType("password", GrantType{
paddy@119 24 Validate: credentialsValidate,
paddy@119 25 Invalidate: nil,
paddy@119 26 IssuesRefresh: true,
paddy@119 27 ReturnToken: RenderJSONToken,
paddy@124 28 AuditString: credentialsAuditString,
paddy@119 29 })
paddy@119 30 }
paddy@119 31
paddy@70 32 var (
paddy@70 33 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
paddy@70 34 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
paddy@70 35 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
paddy@70 36 ErrSessionNotFound = errors.New("session not found in sessionStore")
paddy@70 37 // ErrInvalidSession is returned when a Session is specified but is not valid.
paddy@70 38 ErrInvalidSession = errors.New("session is not valid")
paddy@77 39 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
paddy@77 40 ErrSessionAlreadyExists = errors.New("session already exists")
paddy@98 41
paddy@98 42 passphraseSchemes = map[int]passphraseScheme{
paddy@98 43 1: {
paddy@98 44 check: pbkdf2sha256check,
paddy@98 45 create: pbkdf2sha256create,
paddy@98 46 calculateIterations: pbkdf2sha256calc,
paddy@98 47 },
paddy@98 48 }
paddy@70 49 )
paddy@70 50
paddy@98 51 type passphraseScheme struct {
paddy@98 52 check func(profile Profile, passphrase string) (bool, error)
paddy@103 53 create func(passphrase string, iterations int) (result, salt string, err error)
paddy@98 54 calculateIterations func() (int, error)
paddy@98 55 }
paddy@98 56
paddy@70 57 // Session represents a user's authenticated session, associating it with a profile
paddy@70 58 // and some audit data.
paddy@70 59 type Session struct {
paddy@70 60 ID string
paddy@70 61 IP string
paddy@70 62 UserAgent string
paddy@70 63 ProfileID uuid.ID
paddy@98 64 Login string
paddy@70 65 Created time.Time
paddy@70 66 Active bool
paddy@70 67 }
paddy@70 68
paddy@89 69 type sortedSessions []Session
paddy@89 70
paddy@89 71 func (s sortedSessions) Len() int {
paddy@89 72 return len(s)
paddy@89 73 }
paddy@89 74
paddy@89 75 func (s sortedSessions) Less(i, j int) bool {
paddy@89 76 return s[i].Created.After(s[j].Created)
paddy@89 77 }
paddy@89 78
paddy@89 79 func (s sortedSessions) Swap(i, j int) {
paddy@89 80 s[i], s[j] = s[j], s[i]
paddy@89 81 }
paddy@89 82
paddy@70 83 type sessionStore interface {
paddy@70 84 createSession(session Session) error
paddy@70 85 getSession(id string) (Session, error)
paddy@70 86 removeSession(id string) error
paddy@70 87 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
paddy@70 88 }
paddy@77 89
paddy@77 90 func (m *memstore) createSession(session Session) error {
paddy@77 91 m.sessionLock.Lock()
paddy@77 92 defer m.sessionLock.Unlock()
paddy@77 93 if _, ok := m.sessions[session.ID]; ok {
paddy@77 94 return ErrSessionAlreadyExists
paddy@77 95 }
paddy@77 96 m.sessions[session.ID] = session
paddy@77 97 return nil
paddy@77 98 }
paddy@77 99
paddy@77 100 func (m *memstore) getSession(id string) (Session, error) {
paddy@77 101 m.sessionLock.RLock()
paddy@77 102 defer m.sessionLock.RUnlock()
paddy@77 103 if _, ok := m.sessions[id]; !ok {
paddy@77 104 return Session{}, ErrSessionNotFound
paddy@77 105 }
paddy@77 106 return m.sessions[id], nil
paddy@77 107 }
paddy@77 108
paddy@77 109 func (m *memstore) removeSession(id string) error {
paddy@77 110 m.sessionLock.Lock()
paddy@77 111 defer m.sessionLock.Unlock()
paddy@77 112 if _, ok := m.sessions[id]; !ok {
paddy@77 113 return ErrSessionNotFound
paddy@77 114 }
paddy@77 115 delete(m.sessions, id)
paddy@77 116 return nil
paddy@77 117 }
paddy@77 118
paddy@77 119 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
paddy@77 120 m.sessionLock.RLock()
paddy@77 121 defer m.sessionLock.RUnlock()
paddy@77 122 res := []Session{}
paddy@77 123 for _, session := range m.sessions {
paddy@77 124 if int64(len(res)) >= num {
paddy@77 125 break
paddy@77 126 }
paddy@77 127 if profile != nil && !profile.Equal(session.ProfileID) {
paddy@77 128 continue
paddy@77 129 }
paddy@77 130 if !before.IsZero() && session.Created.After(before) {
paddy@77 131 continue
paddy@77 132 }
paddy@77 133 res = append(res, session)
paddy@77 134 }
paddy@89 135 sorted := sortedSessions(res)
paddy@89 136 sort.Sort(sorted)
paddy@89 137 res = []Session(sorted)
paddy@77 138 return res, nil
paddy@77 139 }
paddy@98 140
paddy@98 141 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
paddy@98 142 func RegisterSessionHandlers(r *mux.Router, context Context) {
paddy@98 143 r.Handle("/login", wrap(context, CreateSessionHandler))
paddy@128 144 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
paddy@128 145 // BUG(paddy): We need to implement a handler for terminating sessions.
paddy@98 146 }
paddy@98 147
paddy@98 148 func checkCookie(r *http.Request, context Context) (Session, error) {
paddy@98 149 cookie, err := r.Cookie(authCookieName)
paddy@98 150 if err == http.ErrNoCookie {
paddy@98 151 return Session{}, ErrNoSession
paddy@98 152 } else if err != nil {
paddy@98 153 log.Println(err)
paddy@98 154 return Session{}, err
paddy@98 155 }
paddy@98 156 sess, err := context.GetSession(cookie.Value)
paddy@98 157 if err == ErrSessionNotFound {
paddy@98 158 return Session{}, ErrInvalidSession
paddy@98 159 } else if err != nil {
paddy@98 160 return Session{}, err
paddy@98 161 }
paddy@98 162 if !sess.Active {
paddy@98 163 return Session{}, ErrInvalidSession
paddy@98 164 }
paddy@98 165 return sess, nil
paddy@98 166 }
paddy@98 167
paddy@98 168 func buildLoginRedirect(r *http.Request, context Context) string {
paddy@98 169 if context.loginURI == nil {
paddy@98 170 return ""
paddy@98 171 }
paddy@98 172 uri := *context.loginURI
paddy@98 173 q := uri.Query()
paddy@98 174 q.Set("from", r.URL.String())
paddy@98 175 uri.RawQuery = q.Encode()
paddy@98 176 return uri.String()
paddy@98 177 }
paddy@98 178
paddy@98 179 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
paddy@98 180 realPass, err := hex.DecodeString(profile.Passphrase)
paddy@98 181 if err != nil {
paddy@98 182 return false, err
paddy@98 183 }
paddy@103 184 realSalt, err := hex.DecodeString(profile.Salt)
paddy@103 185 if err != nil {
paddy@103 186 return false, err
paddy@103 187 }
paddy@103 188 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
paddy@98 189 if !pass.Compare(candidate, realPass) {
paddy@98 190 return false, ErrIncorrectAuth
paddy@98 191 }
paddy@98 192 return true, nil
paddy@98 193 }
paddy@98 194
paddy@103 195 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
paddy@103 196 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
paddy@103 197 if err != nil {
paddy@103 198 return "", "", err
paddy@103 199 }
paddy@103 200 result = hex.EncodeToString(passBytes)
paddy@103 201 salt = hex.EncodeToString(saltBytes)
paddy@103 202 return result, salt, err
paddy@98 203 }
paddy@98 204
paddy@98 205 func pbkdf2sha256calc() (int, error) {
paddy@98 206 return pass.CalculateIterations(sha256.New)
paddy@98 207 }
paddy@98 208
paddy@98 209 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@98 210 profile, err := context.GetProfileByLogin(user)
paddy@98 211 if err != nil {
paddy@98 212 if err == ErrProfileNotFound || err == ErrLoginNotFound {
paddy@98 213 return Profile{}, ErrIncorrectAuth
paddy@98 214 }
paddy@98 215 return Profile{}, err
paddy@98 216 }
paddy@98 217 if profile.Compromised {
paddy@98 218 return Profile{}, ErrProfileCompromised
paddy@98 219 }
paddy@98 220 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
paddy@98 221 return profile, ErrProfileLocked
paddy@98 222 }
paddy@98 223 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
paddy@98 224 if !ok {
paddy@98 225 return Profile{}, ErrInvalidPassphraseScheme
paddy@98 226 }
paddy@98 227 result, err := scheme.check(profile, passphrase)
paddy@98 228 if !result {
paddy@98 229 return Profile{}, err
paddy@98 230 }
paddy@98 231 return profile, nil
paddy@98 232 }
paddy@98 233
paddy@98 234 // CreateSessionHandler allows the user to log into their account and create their session.
paddy@98 235 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@98 236 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit
paddy@98 237 errors := []error{}
paddy@98 238 if r.Method == "POST" {
paddy@98 239 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
paddy@98 240 if err == nil {
paddy@98 241 ip := r.Header.Get("X-Forwarded-For")
paddy@98 242 if ip == "" {
paddy@98 243 ip = r.RemoteAddr
paddy@98 244 }
paddy@98 245 session := Session{
paddy@98 246 ID: uuid.NewID().String(),
paddy@98 247 IP: ip,
paddy@98 248 UserAgent: r.UserAgent(),
paddy@98 249 ProfileID: profile.ID,
paddy@98 250 Login: r.PostFormValue("login"),
paddy@98 251 Created: time.Now(),
paddy@98 252 Active: true,
paddy@98 253 }
paddy@98 254 err = context.CreateSession(session)
paddy@98 255 if err != nil {
paddy@98 256 w.WriteHeader(http.StatusInternalServerError)
paddy@98 257 w.Write([]byte(err.Error()))
paddy@98 258 return
paddy@98 259 }
paddy@128 260 // BUG(paddy): We really need to do a security audit on our cookie.
paddy@98 261 cookie := http.Cookie{
paddy@98 262 Name: authCookieName,
paddy@98 263 Value: session.ID,
paddy@98 264 Expires: time.Now().Add(24 * 7 * time.Hour),
paddy@98 265 HttpOnly: true,
paddy@98 266 }
paddy@98 267 http.SetCookie(w, &cookie)
paddy@98 268 redirectTo := r.URL.Query().Get("from")
paddy@98 269 if redirectTo == "" {
paddy@98 270 redirectTo = "/"
paddy@98 271 }
paddy@98 272 http.Redirect(w, r, redirectTo, http.StatusFound)
paddy@98 273 return
paddy@98 274 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
paddy@98 275 w.WriteHeader(http.StatusInternalServerError)
paddy@98 276 w.Write([]byte(err.Error()))
paddy@98 277 return
paddy@98 278 } else {
paddy@98 279 errors = append(errors, err)
paddy@98 280 }
paddy@98 281 }
paddy@98 282 context.Render(w, loginTemplateName, map[string]interface{}{
paddy@98 283 "errors": errors,
paddy@98 284 })
paddy@98 285 }
paddy@119 286
paddy@119 287 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
paddy@119 288 enc := json.NewEncoder(w)
paddy@119 289 username := r.PostFormValue("username")
paddy@119 290 password := r.PostFormValue("password")
paddy@119 291 scope = r.PostFormValue("scope")
paddy@119 292 profile, err := authenticate(username, password, context)
paddy@119 293 if err != nil {
paddy@119 294 if err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked {
paddy@119 295 w.WriteHeader(http.StatusBadRequest)
paddy@119 296 renderJSONError(enc, "invalid_grant")
paddy@119 297 return
paddy@119 298 }
paddy@119 299 w.WriteHeader(http.StatusInternalServerError)
paddy@119 300 w.Write([]byte(err.Error()))
paddy@119 301 return
paddy@119 302 }
paddy@119 303 profileID = profile.ID
paddy@119 304 valid = true
paddy@119 305 return
paddy@119 306 }
paddy@124 307
paddy@124 308 func credentialsAuditString(r *http.Request) string {
paddy@124 309 return "credentials"
paddy@124 310 }