auth
auth/session.go
Enable CSRF protection, add expiration to sessions. Sessions gain a CSRF token, which is passed as a parameter to the login page. The login page now checks for that CSRF token, and logs a CSRF attempt if the token does not match. I also added an expiration to sessions, so they don't last forever. Sessions should be pretty short--we just need to stay logged in for long enough to approve the OAuth request. Everything after that should be cookie based. Finally, I added a configuration parameter to control whether the session cookie should be set to Secure, requiring the use of HTTPS. For production use, this flag is a requirement, but it makes testing extremely difficult, so we need a way to disable it.
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 }