auth

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

130:6c755b23ec80 Go to Latest

auth/session.go

Change normalization flags to a constant. Let's use a constant so we can ensure we're using the same flags everywhere. Otherwise, we can get weird data corruption because we use the wrong flags.

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 }