auth

Paddy 2015-01-18 Parent:118a69954621 Child:d14f0a81498c

123:0a1e16b9c141 Go to Latest

auth/session.go

Refactor verifyClient, implement refresh tokens. Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient out of each of the GrantType's validation functions and into the access token endpoint, where it will be called before the GrantType's validation function. Yay, less code repetition. And seeing as we always want to verify the client, that seems like a good way to prevent things like 118a69954621 from happening. This did, however, force us to add an AllowsPublic property to the GrantType, so the token endpoint knows whether or not a public Client is valid for any given GrantType. We also implemented the refresh token grant type, which required adding ClientID and RefreshRevoked as properties on the Token type. We need ClientID because we need to constrain refresh tokens to the client that issued them. We also should probably keep track of which tokens belong to which clients, just as a general rule of thumb. RefreshRevoked had to be created, next to Revoked, because the AccessToken could be revoked and the RefreshToken still valid, or vice versa. Notably, when you issue a new refresh token, the old one is revoked, but the access token is still valid. It remains to be seen whether this is a good way to track things or not. The number of duplicated properties lead me to believe our type is not a great representation of the underlying concepts.

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