auth

Paddy 2015-01-24 Parent:23c1a07c8a61 Child:163ce22fa4c9

129:4f5d13d2f7c7 Go to Latest

auth/session.go

Test our getClientAuth helper, switch to table-based tests. Our getClientAuth helper was being tested implicitly when we tested our verifyClient helper, but let's test them separately. While we're at it, let's use table based tests instead of copy and paste. I noticed a lot of copy/paste errors while I was updating this, and the less test code we have and the easier we make it to test new edge cases, the better of we are.

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