auth

Paddy 2015-03-03 Parent:163ce22fa4c9 Child:026adb0c7fc4

135:d30a3a12d387 Go to Latest

auth/session.go

Attach our Scope type to AuthCodes and Tokens. When obtaining an AuthorizationCode or Token, attach a slice of strings, each one a Scope ID, instead of just attaching the encoded string the user passes in. This will allow us to change our Scope encoding down the line, and is more conceptually faithful. Also, if an authorization request is made with an invalid scope, return the invalid_scope error.

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@135 13 "strings"
paddy@70 14 "time"
paddy@70 15
paddy@107 16 "code.secondbit.org/pass.hg"
paddy@107 17 "code.secondbit.org/uuid.hg"
paddy@98 18 "github.com/gorilla/mux"
paddy@98 19 )
paddy@98 20
paddy@98 21 const (
paddy@132 22 authCookieName = "auth"
paddy@98 23 loginTemplateName = "login"
paddy@70 24 )
paddy@70 25
paddy@119 26 func init() {
paddy@119 27 RegisterGrantType("password", GrantType{
paddy@119 28 Validate: credentialsValidate,
paddy@119 29 Invalidate: nil,
paddy@119 30 IssuesRefresh: true,
paddy@119 31 ReturnToken: RenderJSONToken,
paddy@124 32 AuditString: credentialsAuditString,
paddy@119 33 })
paddy@119 34 }
paddy@119 35
paddy@70 36 var (
paddy@70 37 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
paddy@70 38 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
paddy@70 39 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
paddy@70 40 ErrSessionNotFound = errors.New("session not found in sessionStore")
paddy@70 41 // ErrInvalidSession is returned when a Session is specified but is not valid.
paddy@70 42 ErrInvalidSession = errors.New("session is not valid")
paddy@77 43 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
paddy@77 44 ErrSessionAlreadyExists = errors.New("session already exists")
paddy@132 45 // ErrCSRFAttempt is returned when a CSRF attempt is detected.
paddy@132 46 ErrCSRFAttempt = errors.New("CSRF attempt")
paddy@98 47
paddy@98 48 passphraseSchemes = map[int]passphraseScheme{
paddy@98 49 1: {
paddy@98 50 check: pbkdf2sha256check,
paddy@98 51 create: pbkdf2sha256create,
paddy@98 52 calculateIterations: pbkdf2sha256calc,
paddy@98 53 },
paddy@98 54 }
paddy@70 55 )
paddy@70 56
paddy@98 57 type passphraseScheme struct {
paddy@98 58 check func(profile Profile, passphrase string) (bool, error)
paddy@103 59 create func(passphrase string, iterations int) (result, salt string, err error)
paddy@98 60 calculateIterations func() (int, error)
paddy@98 61 }
paddy@98 62
paddy@70 63 // Session represents a user's authenticated session, associating it with a profile
paddy@70 64 // and some audit data.
paddy@70 65 type Session struct {
paddy@70 66 ID string
paddy@70 67 IP string
paddy@70 68 UserAgent string
paddy@70 69 ProfileID uuid.ID
paddy@98 70 Login string
paddy@70 71 Created time.Time
paddy@132 72 Expires time.Time
paddy@70 73 Active bool
paddy@132 74 CSRFToken string
paddy@70 75 }
paddy@70 76
paddy@89 77 type sortedSessions []Session
paddy@89 78
paddy@89 79 func (s sortedSessions) Len() int {
paddy@89 80 return len(s)
paddy@89 81 }
paddy@89 82
paddy@89 83 func (s sortedSessions) Less(i, j int) bool {
paddy@89 84 return s[i].Created.After(s[j].Created)
paddy@89 85 }
paddy@89 86
paddy@89 87 func (s sortedSessions) Swap(i, j int) {
paddy@89 88 s[i], s[j] = s[j], s[i]
paddy@89 89 }
paddy@89 90
paddy@70 91 type sessionStore interface {
paddy@70 92 createSession(session Session) error
paddy@70 93 getSession(id string) (Session, error)
paddy@70 94 removeSession(id string) error
paddy@70 95 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
paddy@70 96 }
paddy@77 97
paddy@77 98 func (m *memstore) createSession(session Session) error {
paddy@77 99 m.sessionLock.Lock()
paddy@77 100 defer m.sessionLock.Unlock()
paddy@77 101 if _, ok := m.sessions[session.ID]; ok {
paddy@77 102 return ErrSessionAlreadyExists
paddy@77 103 }
paddy@77 104 m.sessions[session.ID] = session
paddy@77 105 return nil
paddy@77 106 }
paddy@77 107
paddy@77 108 func (m *memstore) getSession(id string) (Session, error) {
paddy@77 109 m.sessionLock.RLock()
paddy@77 110 defer m.sessionLock.RUnlock()
paddy@77 111 if _, ok := m.sessions[id]; !ok {
paddy@77 112 return Session{}, ErrSessionNotFound
paddy@77 113 }
paddy@77 114 return m.sessions[id], nil
paddy@77 115 }
paddy@77 116
paddy@77 117 func (m *memstore) removeSession(id string) error {
paddy@77 118 m.sessionLock.Lock()
paddy@77 119 defer m.sessionLock.Unlock()
paddy@77 120 if _, ok := m.sessions[id]; !ok {
paddy@77 121 return ErrSessionNotFound
paddy@77 122 }
paddy@77 123 delete(m.sessions, id)
paddy@77 124 return nil
paddy@77 125 }
paddy@77 126
paddy@77 127 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
paddy@77 128 m.sessionLock.RLock()
paddy@77 129 defer m.sessionLock.RUnlock()
paddy@77 130 res := []Session{}
paddy@77 131 for _, session := range m.sessions {
paddy@77 132 if int64(len(res)) >= num {
paddy@77 133 break
paddy@77 134 }
paddy@77 135 if profile != nil && !profile.Equal(session.ProfileID) {
paddy@77 136 continue
paddy@77 137 }
paddy@77 138 if !before.IsZero() && session.Created.After(before) {
paddy@77 139 continue
paddy@77 140 }
paddy@77 141 res = append(res, session)
paddy@77 142 }
paddy@89 143 sorted := sortedSessions(res)
paddy@89 144 sort.Sort(sorted)
paddy@89 145 res = []Session(sorted)
paddy@77 146 return res, nil
paddy@77 147 }
paddy@98 148
paddy@98 149 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
paddy@98 150 func RegisterSessionHandlers(r *mux.Router, context Context) {
paddy@98 151 r.Handle("/login", wrap(context, CreateSessionHandler))
paddy@128 152 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
paddy@128 153 // BUG(paddy): We need to implement a handler for terminating sessions.
paddy@98 154 }
paddy@98 155
paddy@132 156 func checkCSRF(r *http.Request, s Session) error {
paddy@132 157 if r.PostFormValue("csrftoken") != s.CSRFToken {
paddy@132 158 return ErrCSRFAttempt
paddy@132 159 }
paddy@132 160 return nil
paddy@132 161 }
paddy@132 162
paddy@98 163 func checkCookie(r *http.Request, context Context) (Session, error) {
paddy@98 164 cookie, err := r.Cookie(authCookieName)
paddy@98 165 if err == http.ErrNoCookie {
paddy@98 166 return Session{}, ErrNoSession
paddy@98 167 } else if err != nil {
paddy@98 168 log.Println(err)
paddy@98 169 return Session{}, err
paddy@98 170 }
paddy@98 171 sess, err := context.GetSession(cookie.Value)
paddy@98 172 if err == ErrSessionNotFound {
paddy@98 173 return Session{}, ErrInvalidSession
paddy@98 174 } else if err != nil {
paddy@98 175 return Session{}, err
paddy@98 176 }
paddy@98 177 if !sess.Active {
paddy@98 178 return Session{}, ErrInvalidSession
paddy@98 179 }
paddy@132 180 if time.Now().After(sess.Expires) {
paddy@132 181 return Session{}, ErrInvalidSession
paddy@132 182 }
paddy@98 183 return sess, nil
paddy@98 184 }
paddy@98 185
paddy@98 186 func buildLoginRedirect(r *http.Request, context Context) string {
paddy@98 187 if context.loginURI == nil {
paddy@98 188 return ""
paddy@98 189 }
paddy@98 190 uri := *context.loginURI
paddy@98 191 q := uri.Query()
paddy@98 192 q.Set("from", r.URL.String())
paddy@98 193 uri.RawQuery = q.Encode()
paddy@98 194 return uri.String()
paddy@98 195 }
paddy@98 196
paddy@98 197 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
paddy@98 198 realPass, err := hex.DecodeString(profile.Passphrase)
paddy@98 199 if err != nil {
paddy@98 200 return false, err
paddy@98 201 }
paddy@103 202 realSalt, err := hex.DecodeString(profile.Salt)
paddy@103 203 if err != nil {
paddy@103 204 return false, err
paddy@103 205 }
paddy@103 206 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
paddy@98 207 if !pass.Compare(candidate, realPass) {
paddy@98 208 return false, ErrIncorrectAuth
paddy@98 209 }
paddy@98 210 return true, nil
paddy@98 211 }
paddy@98 212
paddy@103 213 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
paddy@103 214 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
paddy@103 215 if err != nil {
paddy@103 216 return "", "", err
paddy@103 217 }
paddy@103 218 result = hex.EncodeToString(passBytes)
paddy@103 219 salt = hex.EncodeToString(saltBytes)
paddy@103 220 return result, salt, err
paddy@98 221 }
paddy@98 222
paddy@98 223 func pbkdf2sha256calc() (int, error) {
paddy@98 224 return pass.CalculateIterations(sha256.New)
paddy@98 225 }
paddy@98 226
paddy@98 227 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@98 228 profile, err := context.GetProfileByLogin(user)
paddy@98 229 if err != nil {
paddy@98 230 if err == ErrProfileNotFound || err == ErrLoginNotFound {
paddy@98 231 return Profile{}, ErrIncorrectAuth
paddy@98 232 }
paddy@98 233 return Profile{}, err
paddy@98 234 }
paddy@98 235 if profile.Compromised {
paddy@98 236 return Profile{}, ErrProfileCompromised
paddy@98 237 }
paddy@98 238 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
paddy@98 239 return profile, ErrProfileLocked
paddy@98 240 }
paddy@98 241 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
paddy@98 242 if !ok {
paddy@98 243 return Profile{}, ErrInvalidPassphraseScheme
paddy@98 244 }
paddy@98 245 result, err := scheme.check(profile, passphrase)
paddy@98 246 if !result {
paddy@98 247 return Profile{}, err
paddy@98 248 }
paddy@98 249 return profile, nil
paddy@98 250 }
paddy@98 251
paddy@98 252 // CreateSessionHandler allows the user to log into their account and create their session.
paddy@98 253 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@98 254 errors := []error{}
paddy@98 255 if r.Method == "POST" {
paddy@98 256 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
paddy@98 257 if err == nil {
paddy@98 258 ip := r.Header.Get("X-Forwarded-For")
paddy@98 259 if ip == "" {
paddy@98 260 ip = r.RemoteAddr
paddy@98 261 }
paddy@132 262 sessionID := make([]byte, 32)
paddy@132 263 csrfToken := make([]byte, 32)
paddy@132 264 _, err = rand.Read(sessionID)
paddy@132 265 if err != nil {
paddy@132 266 log.Println("Error reading CSPRNG for session ID:", err)
paddy@132 267 w.WriteHeader(http.StatusInternalServerError)
paddy@132 268 w.Write([]byte("Internal error"))
paddy@132 269 return
paddy@132 270 }
paddy@132 271 _, err = rand.Read(csrfToken)
paddy@132 272 if err != nil {
paddy@132 273 log.Println("Error reading CSPRNG for CSRF token:", err)
paddy@132 274 w.WriteHeader(http.StatusInternalServerError)
paddy@132 275 w.Write([]byte("internal error"))
paddy@132 276 return
paddy@132 277 }
paddy@98 278 session := Session{
paddy@132 279 ID: base64.StdEncoding.EncodeToString(sessionID),
paddy@98 280 IP: ip,
paddy@98 281 UserAgent: r.UserAgent(),
paddy@98 282 ProfileID: profile.ID,
paddy@98 283 Login: r.PostFormValue("login"),
paddy@98 284 Created: time.Now(),
paddy@132 285 Expires: time.Now().Add(time.Hour),
paddy@98 286 Active: true,
paddy@132 287 CSRFToken: base64.StdEncoding.EncodeToString(csrfToken),
paddy@98 288 }
paddy@98 289 err = context.CreateSession(session)
paddy@98 290 if err != nil {
paddy@98 291 w.WriteHeader(http.StatusInternalServerError)
paddy@98 292 w.Write([]byte(err.Error()))
paddy@98 293 return
paddy@98 294 }
paddy@132 295 // BUG(paddy): We really need to do a security audit on our cookies.
paddy@98 296 cookie := http.Cookie{
paddy@98 297 Name: authCookieName,
paddy@98 298 Value: session.ID,
paddy@132 299 Expires: session.Expires,
paddy@98 300 HttpOnly: true,
paddy@132 301 Secure: context.config.secureCookie,
paddy@98 302 }
paddy@98 303 http.SetCookie(w, &cookie)
paddy@98 304 redirectTo := r.URL.Query().Get("from")
paddy@98 305 if redirectTo == "" {
paddy@98 306 redirectTo = "/"
paddy@98 307 }
paddy@98 308 http.Redirect(w, r, redirectTo, http.StatusFound)
paddy@98 309 return
paddy@98 310 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
paddy@98 311 w.WriteHeader(http.StatusInternalServerError)
paddy@98 312 w.Write([]byte(err.Error()))
paddy@98 313 return
paddy@98 314 } else {
paddy@98 315 errors = append(errors, err)
paddy@98 316 }
paddy@98 317 }
paddy@98 318 context.Render(w, loginTemplateName, map[string]interface{}{
paddy@98 319 "errors": errors,
paddy@98 320 })
paddy@98 321 }
paddy@119 322
paddy@135 323 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
paddy@119 324 enc := json.NewEncoder(w)
paddy@119 325 username := r.PostFormValue("username")
paddy@119 326 password := r.PostFormValue("password")
paddy@135 327 scopes = strings.Split(r.PostFormValue("scope"), " ")
paddy@119 328 profile, err := authenticate(username, password, context)
paddy@119 329 if err != nil {
paddy@119 330 if err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked {
paddy@119 331 w.WriteHeader(http.StatusBadRequest)
paddy@119 332 renderJSONError(enc, "invalid_grant")
paddy@119 333 return
paddy@119 334 }
paddy@119 335 w.WriteHeader(http.StatusInternalServerError)
paddy@119 336 w.Write([]byte(err.Error()))
paddy@119 337 return
paddy@119 338 }
paddy@119 339 profileID = profile.ID
paddy@119 340 valid = true
paddy@119 341 return
paddy@119 342 }
paddy@124 343
paddy@124 344 func credentialsAuditString(r *http.Request) string {
paddy@124 345 return "credentials"
paddy@124 346 }