auth
auth/session.go
Move login concerns to session, add login handler. Move all our helpers for authenticating, building a login redirect, and reading a cookie to session.go. Rewrite our passphrase scheme code so that a scheme is just a struct with three functions for checking a passphrase against a profile object, generating a passphrase, and calculating the number of iterations to use when generating a passphrase. Define an implementation of our passphrase scheme (scheme #1) using PBKDF2 and SHA256. Add a CreateSessionHandler function that logs the user in using their login and passphrase. Add a RegisterSessionHandlers function that adds the session-related handlers (right now, just our CreateSessionHandler) to the specified router.
1 package auth
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "errors"
7 "log"
8 "net/http"
9 "sort"
10 "time"
12 "code.secondbit.org/pass"
13 "code.secondbit.org/uuid"
15 "github.com/gorilla/mux"
16 )
18 const (
19 loginTemplateName = "login"
20 )
22 var (
23 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
24 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
25 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
26 ErrSessionNotFound = errors.New("session not found in sessionStore")
27 // ErrInvalidSession is returned when a Session is specified but is not valid.
28 ErrInvalidSession = errors.New("session is not valid")
29 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
30 ErrSessionAlreadyExists = errors.New("session already exists")
32 passphraseSchemes = map[int]passphraseScheme{
33 1: {
34 check: pbkdf2sha256check,
35 create: pbkdf2sha256create,
36 calculateIterations: pbkdf2sha256calc,
37 },
38 }
39 )
41 type passphraseScheme struct {
42 check func(profile Profile, passphrase string) (bool, error)
43 create func(passphrase string, iterations int) (result, salt []byte, err error)
44 calculateIterations func() (int, error)
45 }
47 // Session represents a user's authenticated session, associating it with a profile
48 // and some audit data.
49 type Session struct {
50 ID string
51 IP string
52 UserAgent string
53 ProfileID uuid.ID
54 Login string
55 Created time.Time
56 Active bool
57 }
59 type sortedSessions []Session
61 func (s sortedSessions) Len() int {
62 return len(s)
63 }
65 func (s sortedSessions) Less(i, j int) bool {
66 return s[i].Created.After(s[j].Created)
67 }
69 func (s sortedSessions) Swap(i, j int) {
70 s[i], s[j] = s[j], s[i]
71 }
73 type sessionStore interface {
74 createSession(session Session) error
75 getSession(id string) (Session, error)
76 removeSession(id string) error
77 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
78 }
80 func (m *memstore) createSession(session Session) error {
81 m.sessionLock.Lock()
82 defer m.sessionLock.Unlock()
83 if _, ok := m.sessions[session.ID]; ok {
84 return ErrSessionAlreadyExists
85 }
86 m.sessions[session.ID] = session
87 return nil
88 }
90 func (m *memstore) getSession(id string) (Session, error) {
91 m.sessionLock.RLock()
92 defer m.sessionLock.RUnlock()
93 if _, ok := m.sessions[id]; !ok {
94 return Session{}, ErrSessionNotFound
95 }
96 return m.sessions[id], nil
97 }
99 func (m *memstore) removeSession(id string) error {
100 m.sessionLock.Lock()
101 defer m.sessionLock.Unlock()
102 if _, ok := m.sessions[id]; !ok {
103 return ErrSessionNotFound
104 }
105 delete(m.sessions, id)
106 return nil
107 }
109 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
110 m.sessionLock.RLock()
111 defer m.sessionLock.RUnlock()
112 res := []Session{}
113 for _, session := range m.sessions {
114 if int64(len(res)) >= num {
115 break
116 }
117 if profile != nil && !profile.Equal(session.ProfileID) {
118 continue
119 }
120 if !before.IsZero() && session.Created.After(before) {
121 continue
122 }
123 res = append(res, session)
124 }
125 sorted := sortedSessions(res)
126 sort.Sort(sorted)
127 res = []Session(sorted)
128 return res, nil
129 }
131 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
132 func RegisterSessionHandlers(r *mux.Router, context Context) {
133 r.Handle("/login", wrap(context, CreateSessionHandler))
134 }
136 func checkCookie(r *http.Request, context Context) (Session, error) {
137 cookie, err := r.Cookie(authCookieName)
138 if err == http.ErrNoCookie {
139 return Session{}, ErrNoSession
140 } else if err != nil {
141 log.Println(err)
142 return Session{}, err
143 }
144 sess, err := context.GetSession(cookie.Value)
145 if err == ErrSessionNotFound {
146 return Session{}, ErrInvalidSession
147 } else if err != nil {
148 return Session{}, err
149 }
150 if !sess.Active {
151 return Session{}, ErrInvalidSession
152 }
153 return sess, nil
154 }
156 func buildLoginRedirect(r *http.Request, context Context) string {
157 if context.loginURI == nil {
158 return ""
159 }
160 uri := *context.loginURI
161 q := uri.Query()
162 q.Set("from", r.URL.String())
163 uri.RawQuery = q.Encode()
164 return uri.String()
165 }
167 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
168 realPass, err := hex.DecodeString(profile.Passphrase)
169 if err != nil {
170 return false, err
171 }
172 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
173 if !pass.Compare(candidate, realPass) {
174 return false, ErrIncorrectAuth
175 }
176 return true, nil
177 }
179 func pbkdf2sha256create(passphrase string, iters int) (result, salt []byte, err error) {
180 return pass.Create(sha256.New, iters, []byte(passphrase))
181 }
183 func pbkdf2sha256calc() (int, error) {
184 return pass.CalculateIterations(sha256.New)
185 }
187 func authenticate(user, passphrase string, context Context) (Profile, error) {
188 profile, err := context.GetProfileByLogin(user)
189 if err != nil {
190 if err == ErrProfileNotFound || err == ErrLoginNotFound {
191 return Profile{}, ErrIncorrectAuth
192 }
193 return Profile{}, err
194 }
195 if profile.Compromised {
196 return Profile{}, ErrProfileCompromised
197 }
198 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
199 return profile, ErrProfileLocked
200 }
201 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
202 if !ok {
203 return Profile{}, ErrInvalidPassphraseScheme
204 }
205 result, err := scheme.check(profile, passphrase)
206 if !result {
207 return Profile{}, err
208 }
209 return profile, nil
210 }
212 // CreateSessionHandler allows the user to log into their account and create their session.
213 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
214 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit
215 errors := []error{}
216 if r.Method == "POST" {
217 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
218 if err == nil {
219 ip := r.Header.Get("X-Forwarded-For")
220 if ip == "" {
221 ip = r.RemoteAddr
222 }
223 session := Session{
224 ID: uuid.NewID().String(),
225 IP: ip,
226 UserAgent: r.UserAgent(),
227 ProfileID: profile.ID,
228 Login: r.PostFormValue("login"),
229 Created: time.Now(),
230 Active: true,
231 }
232 err = context.CreateSession(session)
233 if err != nil {
234 w.WriteHeader(http.StatusInternalServerError)
235 w.Write([]byte(err.Error()))
236 return
237 }
238 // BUG(paddy): really need to do a security audit on our cookie
239 cookie := http.Cookie{
240 Name: authCookieName,
241 Value: session.ID,
242 Expires: time.Now().Add(24 * 7 * time.Hour),
243 HttpOnly: true,
244 }
245 http.SetCookie(w, &cookie)
246 redirectTo := r.URL.Query().Get("from")
247 if redirectTo == "" {
248 redirectTo = "/"
249 }
250 http.Redirect(w, r, redirectTo, http.StatusFound)
251 return
252 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
253 w.WriteHeader(http.StatusInternalServerError)
254 w.Write([]byte(err.Error()))
255 return
256 } else {
257 errors = append(errors, err)
258 }
259 }
260 context.Render(w, loginTemplateName, map[string]interface{}{
261 "errors": errors,
262 })
263 }