auth

Paddy 2015-01-29 Parent:163ce22fa4c9 Child:d30a3a12d387

133:bc842183181d Go to Latest

auth/session.go

Add Client updating from the API. Add a handler to update Clients using the API. Add a helper that will decode a request for us based on its Content-Type header. Change the ClientChange.Validate function to return as many errors as possible, as opposed to just the first error it encounters. Update the ClientChange.Validate tests to take advantage of the new signature.

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