auth

Paddy 2014-12-14 Parent:09c47387e455 Child:0b45e6b9cb94

99:5bccbed6631b Go to Latest

auth/session.go

Add an endpoint to validate and register profiles. Add a newProfileRequest object that defines the user-specified properties of a new Profile. Add a helper that validates a newProfileRequest and modifies it for sanitization, mostly just removing leading and trailing whitespace. Add MaxNameLength, MaxUsernameLength, and MaxEmailLength constants to hold the maximum length for those properties. Add errors to be returned when a users attempts to log in with a profile that is compromised or locked. Add the bare bones of a CreateProfileHandler that validates a profile registration request adn uses it to create a Profile and at least one Login. Create a requestError struct that is used for returning API errors, along with constants for the slugs we'll use to signal those errors.

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@98 43 create func(passphrase string, iterations int) (result, salt []byte, 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@98 172 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
paddy@98 173 if !pass.Compare(candidate, realPass) {
paddy@98 174 return false, ErrIncorrectAuth
paddy@98 175 }
paddy@98 176 return true, nil
paddy@98 177 }
paddy@98 178
paddy@98 179 func pbkdf2sha256create(passphrase string, iters int) (result, salt []byte, err error) {
paddy@98 180 return pass.Create(sha256.New, iters, []byte(passphrase))
paddy@98 181 }
paddy@98 182
paddy@98 183 func pbkdf2sha256calc() (int, error) {
paddy@98 184 return pass.CalculateIterations(sha256.New)
paddy@98 185 }
paddy@98 186
paddy@98 187 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@98 188 profile, err := context.GetProfileByLogin(user)
paddy@98 189 if err != nil {
paddy@98 190 if err == ErrProfileNotFound || err == ErrLoginNotFound {
paddy@98 191 return Profile{}, ErrIncorrectAuth
paddy@98 192 }
paddy@98 193 return Profile{}, err
paddy@98 194 }
paddy@98 195 if profile.Compromised {
paddy@98 196 return Profile{}, ErrProfileCompromised
paddy@98 197 }
paddy@98 198 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
paddy@98 199 return profile, ErrProfileLocked
paddy@98 200 }
paddy@98 201 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
paddy@98 202 if !ok {
paddy@98 203 return Profile{}, ErrInvalidPassphraseScheme
paddy@98 204 }
paddy@98 205 result, err := scheme.check(profile, passphrase)
paddy@98 206 if !result {
paddy@98 207 return Profile{}, err
paddy@98 208 }
paddy@98 209 return profile, nil
paddy@98 210 }
paddy@98 211
paddy@98 212 // CreateSessionHandler allows the user to log into their account and create their session.
paddy@98 213 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@98 214 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit
paddy@98 215 errors := []error{}
paddy@98 216 if r.Method == "POST" {
paddy@98 217 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
paddy@98 218 if err == nil {
paddy@98 219 ip := r.Header.Get("X-Forwarded-For")
paddy@98 220 if ip == "" {
paddy@98 221 ip = r.RemoteAddr
paddy@98 222 }
paddy@98 223 session := Session{
paddy@98 224 ID: uuid.NewID().String(),
paddy@98 225 IP: ip,
paddy@98 226 UserAgent: r.UserAgent(),
paddy@98 227 ProfileID: profile.ID,
paddy@98 228 Login: r.PostFormValue("login"),
paddy@98 229 Created: time.Now(),
paddy@98 230 Active: true,
paddy@98 231 }
paddy@98 232 err = context.CreateSession(session)
paddy@98 233 if err != nil {
paddy@98 234 w.WriteHeader(http.StatusInternalServerError)
paddy@98 235 w.Write([]byte(err.Error()))
paddy@98 236 return
paddy@98 237 }
paddy@98 238 // BUG(paddy): really need to do a security audit on our cookie
paddy@98 239 cookie := http.Cookie{
paddy@98 240 Name: authCookieName,
paddy@98 241 Value: session.ID,
paddy@98 242 Expires: time.Now().Add(24 * 7 * time.Hour),
paddy@98 243 HttpOnly: true,
paddy@98 244 }
paddy@98 245 http.SetCookie(w, &cookie)
paddy@98 246 redirectTo := r.URL.Query().Get("from")
paddy@98 247 if redirectTo == "" {
paddy@98 248 redirectTo = "/"
paddy@98 249 }
paddy@98 250 http.Redirect(w, r, redirectTo, http.StatusFound)
paddy@98 251 return
paddy@98 252 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
paddy@98 253 w.WriteHeader(http.StatusInternalServerError)
paddy@98 254 w.Write([]byte(err.Error()))
paddy@98 255 return
paddy@98 256 } else {
paddy@98 257 errors = append(errors, err)
paddy@98 258 }
paddy@98 259 }
paddy@98 260 context.Render(w, loginTemplateName, map[string]interface{}{
paddy@98 261 "errors": errors,
paddy@98 262 })
paddy@98 263 }