auth

Paddy 2015-01-29 Parent:163ce22fa4c9 Child:d30a3a12d387

133:bc842183181d Go to Latest

auth/session.go

Add Client updating from the API. Add a handler to update Clients using the API. Add a helper that will decode a request for us based on its Content-Type header. Change the ClientChange.Validate function to return as many errors as possible, as opposed to just the first error it encounters. Update the ClientChange.Validate tests to take advantage of the new signature.

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