auth

Paddy 2014-12-14 Parent:09c47387e455 Child:c03b5eb3179e

103:0b45e6b9cb94 Go to Latest

auth/session.go

Store salts and passphrases as hex-encoded strings. Update our passphraseScheme.create function signature to return strings. Hex encode our passphrases and salts when encrypthing them so they're easier to store safely. Decode our salt before using it to check candidate passphrases.

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