auth
auth/session.go
Implement a GetProfileHandler. Create a Handler that will allow us to return details about a Profile. Right now, you only get a single Profile at a time, which is problematic, because it will lead to N+1 requests. But we have no reason to retrieve anyone _else_'s Profile, so it's not like you need to be fetching any Profile other than your own. Also, this requires a Token issued for the Profile in question, which means you're limited to one Profile per request, anyways. Future avenues for exploration may be an admin Token granting access to many Profiles, the unspecified service flow for accessing the API, or simply accepting that name, join date, last active date, and ID are "public information".
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 "strings"
14 "time"
16 "code.secondbit.org/pass.hg"
17 "code.secondbit.org/uuid.hg"
18 "github.com/gorilla/mux"
19 )
21 const (
22 authCookieName = "auth"
23 loginTemplateName = "login"
24 )
26 func init() {
27 RegisterGrantType("password", GrantType{
28 Validate: credentialsValidate,
29 Invalidate: nil,
30 IssuesRefresh: true,
31 ReturnToken: RenderJSONToken,
32 AuditString: credentialsAuditString,
33 })
34 }
36 var (
37 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
38 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
39 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
40 ErrSessionNotFound = errors.New("session not found in sessionStore")
41 // ErrInvalidSession is returned when a Session is specified but is not valid.
42 ErrInvalidSession = errors.New("session is not valid")
43 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
44 ErrSessionAlreadyExists = errors.New("session already exists")
45 // ErrCSRFAttempt is returned when a CSRF attempt is detected.
46 ErrCSRFAttempt = errors.New("CSRF attempt")
48 passphraseSchemes = map[int]passphraseScheme{
49 1: {
50 check: pbkdf2sha256check,
51 create: pbkdf2sha256create,
52 calculateIterations: pbkdf2sha256calc,
53 },
54 }
55 )
57 type passphraseScheme struct {
58 check func(profile Profile, passphrase string) (bool, error)
59 create func(passphrase string, iterations int) (result, salt string, err error)
60 calculateIterations func() (int, error)
61 }
63 // Session represents a user's authenticated session, associating it with a profile
64 // and some audit data.
65 type Session struct {
66 ID string
67 IP string
68 UserAgent string
69 ProfileID uuid.ID
70 Login string
71 Created time.Time
72 Expires time.Time
73 Active bool
74 CSRFToken string
75 }
77 type sortedSessions []Session
79 func (s sortedSessions) Len() int {
80 return len(s)
81 }
83 func (s sortedSessions) Less(i, j int) bool {
84 return s[i].Created.After(s[j].Created)
85 }
87 func (s sortedSessions) Swap(i, j int) {
88 s[i], s[j] = s[j], s[i]
89 }
91 type sessionStore interface {
92 createSession(session Session) error
93 getSession(id string) (Session, error)
94 terminateSession(id string) error
95 removeSession(id string) error
96 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
97 terminateSessionsByProfile(profile uuid.ID) error
98 }
100 func (m *memstore) createSession(session Session) error {
101 m.sessionLock.Lock()
102 defer m.sessionLock.Unlock()
103 if _, ok := m.sessions[session.ID]; ok {
104 return ErrSessionAlreadyExists
105 }
106 m.sessions[session.ID] = session
107 return nil
108 }
110 func (m *memstore) getSession(id string) (Session, error) {
111 m.sessionLock.RLock()
112 defer m.sessionLock.RUnlock()
113 if _, ok := m.sessions[id]; !ok {
114 return Session{}, ErrSessionNotFound
115 }
116 return m.sessions[id], nil
117 }
119 func (m *memstore) terminateSession(id string) error {
120 m.sessionLock.RLock()
121 defer m.sessionLock.RUnlock()
122 sess, ok := m.sessions[id]
123 if !ok {
124 return ErrSessionNotFound
125 }
126 sess.Active = false
127 m.sessions[id] = sess
128 return nil
129 }
131 func (m *memstore) terminateSessionsByProfile(profile uuid.ID) error {
132 m.sessionLock.RLock()
133 defer m.sessionLock.RUnlock()
134 var found bool
135 for _, session := range m.sessions {
136 if profile.Equal(session.ProfileID) {
137 session.Active = false
138 m.sessions[session.ID] = session
139 found = true
140 }
141 }
142 if !found {
143 return ErrProfileNotFound
144 }
145 return nil
146 }
148 func (m *memstore) removeSession(id string) error {
149 m.sessionLock.Lock()
150 defer m.sessionLock.Unlock()
151 if _, ok := m.sessions[id]; !ok {
152 return ErrSessionNotFound
153 }
154 delete(m.sessions, id)
155 return nil
156 }
158 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
159 m.sessionLock.RLock()
160 defer m.sessionLock.RUnlock()
161 res := []Session{}
162 for _, session := range m.sessions {
163 if int64(len(res)) >= num {
164 break
165 }
166 if profile != nil && !profile.Equal(session.ProfileID) {
167 continue
168 }
169 if !before.IsZero() && session.Created.After(before) {
170 continue
171 }
172 res = append(res, session)
173 }
174 sorted := sortedSessions(res)
175 sort.Sort(sorted)
176 res = []Session(sorted)
177 return res, nil
178 }
180 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
181 func RegisterSessionHandlers(r *mux.Router, context Context) {
182 r.Handle("/login", wrap(context, CreateSessionHandler))
183 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
184 r.Handle("/sessions/{id}", wrap(context, TerminateSessionHandler)).Methods("OPTIONS", "DELETE")
185 }
187 func checkCSRF(r *http.Request, s Session) error {
188 if r.PostFormValue("csrftoken") != s.CSRFToken {
189 return ErrCSRFAttempt
190 }
191 return nil
192 }
194 func checkCookie(r *http.Request, context Context) (Session, error) {
195 cookie, err := r.Cookie(authCookieName)
196 if err == http.ErrNoCookie {
197 return Session{}, ErrNoSession
198 } else if err != nil {
199 log.Println(err)
200 return Session{}, err
201 }
202 sess, err := context.GetSession(cookie.Value)
203 if err == ErrSessionNotFound {
204 return Session{}, ErrInvalidSession
205 } else if err != nil {
206 return Session{}, err
207 }
208 if !sess.Active {
209 return Session{}, ErrInvalidSession
210 }
211 if time.Now().After(sess.Expires) {
212 return Session{}, ErrInvalidSession
213 }
214 return sess, nil
215 }
217 func buildLoginRedirect(r *http.Request, context Context) string {
218 if context.loginURI == nil {
219 return ""
220 }
221 uri := *context.loginURI
222 q := uri.Query()
223 q.Set("from", r.URL.String())
224 uri.RawQuery = q.Encode()
225 return uri.String()
226 }
228 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
229 realPass, err := hex.DecodeString(profile.Passphrase)
230 if err != nil {
231 return false, err
232 }
233 realSalt, err := hex.DecodeString(profile.Salt)
234 if err != nil {
235 return false, err
236 }
237 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
238 if !pass.Compare(candidate, realPass) {
239 return false, ErrIncorrectAuth
240 }
241 return true, nil
242 }
244 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
245 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
246 if err != nil {
247 return "", "", err
248 }
249 result = hex.EncodeToString(passBytes)
250 salt = hex.EncodeToString(saltBytes)
251 return result, salt, err
252 }
254 func pbkdf2sha256calc() (int, error) {
255 return pass.CalculateIterations(sha256.New)
256 }
258 func isAuthError(err error) bool {
259 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
260 }
262 func authenticate(user, passphrase string, context Context) (Profile, error) {
263 profile, err := context.GetProfileByLogin(user)
264 if err != nil {
265 if err == ErrProfileNotFound || err == ErrLoginNotFound {
266 return Profile{}, ErrIncorrectAuth
267 }
268 return Profile{}, err
269 }
270 if profile.Compromised {
271 return Profile{}, ErrProfileCompromised
272 }
273 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
274 return profile, ErrProfileLocked
275 }
276 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
277 if !ok {
278 return Profile{}, ErrInvalidPassphraseScheme
279 }
280 result, err := scheme.check(profile, passphrase)
281 if !result {
282 return Profile{}, err
283 }
284 return profile, nil
285 }
287 // CreateSessionHandler allows the user to log into their account and create their session.
288 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
289 errors := []error{}
290 if r.Method == "POST" {
291 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
292 if err == nil {
293 ip := r.Header.Get("X-Forwarded-For")
294 if ip == "" {
295 ip = r.RemoteAddr
296 }
297 sessionID := make([]byte, 32)
298 csrfToken := make([]byte, 32)
299 _, err = rand.Read(sessionID)
300 if err != nil {
301 log.Println("Error reading CSPRNG for session ID:", err)
302 w.WriteHeader(http.StatusInternalServerError)
303 w.Write([]byte("Internal error"))
304 return
305 }
306 _, err = rand.Read(csrfToken)
307 if err != nil {
308 log.Println("Error reading CSPRNG for CSRF token:", err)
309 w.WriteHeader(http.StatusInternalServerError)
310 w.Write([]byte("internal error"))
311 return
312 }
313 session := Session{
314 ID: base64.URLEncoding.EncodeToString(sessionID),
315 IP: ip,
316 UserAgent: r.UserAgent(),
317 ProfileID: profile.ID,
318 Login: r.PostFormValue("login"),
319 Created: time.Now(),
320 Expires: time.Now().Add(time.Hour),
321 Active: true,
322 CSRFToken: base64.URLEncoding.EncodeToString(csrfToken),
323 }
324 err = context.CreateSession(session)
325 if err != nil {
326 w.WriteHeader(http.StatusInternalServerError)
327 w.Write([]byte(err.Error()))
328 return
329 }
330 // BUG(paddy): We really need to do a security audit on our cookies.
331 cookie := http.Cookie{
332 Name: authCookieName,
333 Value: session.ID,
334 Expires: session.Expires,
335 HttpOnly: true,
336 Secure: context.config.secureCookie,
337 }
338 http.SetCookie(w, &cookie)
339 redirectTo := r.URL.Query().Get("from")
340 if redirectTo == "" {
341 redirectTo = "/"
342 }
343 http.Redirect(w, r, redirectTo, http.StatusFound)
344 return
345 } else if !isAuthError(err) {
346 w.WriteHeader(http.StatusInternalServerError)
347 w.Write([]byte(err.Error()))
348 return
349 } else {
350 errors = append(errors, err)
351 }
352 }
353 context.Render(w, loginTemplateName, map[string]interface{}{
354 "errors": errors,
355 })
356 }
358 // TerminateSessionHandler allows the user to end their session before it expires.
359 func TerminateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
360 var errors []requestError
361 vars := mux.Vars(r)
362 if vars["id"] == "" {
363 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
364 encode(w, r, http.StatusBadRequest, response{Errors: errors})
365 return
366 }
367 id := vars["id"]
368 un, pw, ok := r.BasicAuth()
369 if !ok {
370 errors = append(errors, requestError{Slug: requestErrAccessDenied})
371 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
372 return
373 }
374 profile, err := authenticate(un, pw, context)
375 if err != nil {
376 if isAuthError(err) {
377 errors = append(errors, requestError{Slug: requestErrAccessDenied})
378 encode(w, r, http.StatusForbidden, response{Errors: errors})
379 return
380 }
381 errors = append(errors, requestError{Slug: requestErrActOfGod})
382 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
383 return
384 }
385 session, err := context.GetSession(id)
386 if err != nil {
387 if err == ErrSessionNotFound {
388 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
389 encode(w, r, http.StatusNotFound, response{Errors: errors})
390 return
391 }
392 errors = append(errors, requestError{Slug: requestErrActOfGod})
393 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
394 return
395 }
396 if !session.ProfileID.Equal(profile.ID) {
397 errors = append(errors, requestError{Slug: requestErrAccessDenied, Param: "id"})
398 encode(w, r, http.StatusForbidden, response{Errors: errors})
399 return
400 }
401 err = context.TerminateSession(id)
402 if err != nil {
403 if err == ErrSessionNotFound {
404 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
405 encode(w, r, http.StatusNotFound, response{Errors: errors})
406 return
407 }
408 errors = append(errors, requestError{Slug: requestErrActOfGod})
409 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
410 return
411 }
412 session.Active = false
413 encode(w, r, http.StatusOK, response{Sessions: []Session{session}, Errors: errors})
414 }
416 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
417 enc := json.NewEncoder(w)
418 username := r.PostFormValue("username")
419 password := r.PostFormValue("password")
420 scopes = stringsToScopes(strings.Split(r.PostFormValue("scope"), " "))
421 profile, err := authenticate(username, password, context)
422 if err != nil {
423 if isAuthError(err) {
424 w.WriteHeader(http.StatusBadRequest)
425 renderJSONError(enc, "invalid_grant")
426 return
427 }
428 w.WriteHeader(http.StatusInternalServerError)
429 w.Write([]byte(err.Error()))
430 return
431 }
432 profileID = profile.ID
433 valid = true
434 return
435 }
437 func credentialsAuditString(r *http.Request) string {
438 return "credentials"
439 }