auth

Paddy 2015-01-10 Parent:c03b5eb3179e Child:3ec7134fa211

112:da2a0954e8d3 Go to Latest

auth/session.go

Flesh out auth code grant (in)validation. Flesh out our tests for functions that do the validation and invalidation of the authorization code grant type's authorization codes. Basically, make sure that the auth code's are being checked right and that marking them as used after they're used works.

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