auth

Paddy 2015-02-20 Parent:163ce22fa4c9 Child:d30a3a12d387

134:d103a598548c Go to Latest

auth/session.go

Introduced scopes. Created a Scope type and a scopeStore interface, along with the memstore methods for the scopeStore. This will allow applications to define access with granularity, so users can grant access to some data, not _all_ data. We're operating on the assumption that there won't be an unreasonable number of scopes defined, so there is no paging operation included for the ListScopes method. This is a decision that may have to be revisited in the future, depending on usecases.

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 }