auth
auth/session.go
Require authentication to update Clients. Require the Client's owner to supply basic authentication when updating a client.
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 removeSession(id string) error
95 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
96 }
98 func (m *memstore) createSession(session Session) error {
99 m.sessionLock.Lock()
100 defer m.sessionLock.Unlock()
101 if _, ok := m.sessions[session.ID]; ok {
102 return ErrSessionAlreadyExists
103 }
104 m.sessions[session.ID] = session
105 return nil
106 }
108 func (m *memstore) getSession(id string) (Session, error) {
109 m.sessionLock.RLock()
110 defer m.sessionLock.RUnlock()
111 if _, ok := m.sessions[id]; !ok {
112 return Session{}, ErrSessionNotFound
113 }
114 return m.sessions[id], nil
115 }
117 func (m *memstore) removeSession(id string) error {
118 m.sessionLock.Lock()
119 defer m.sessionLock.Unlock()
120 if _, ok := m.sessions[id]; !ok {
121 return ErrSessionNotFound
122 }
123 delete(m.sessions, id)
124 return nil
125 }
127 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
128 m.sessionLock.RLock()
129 defer m.sessionLock.RUnlock()
130 res := []Session{}
131 for _, session := range m.sessions {
132 if int64(len(res)) >= num {
133 break
134 }
135 if profile != nil && !profile.Equal(session.ProfileID) {
136 continue
137 }
138 if !before.IsZero() && session.Created.After(before) {
139 continue
140 }
141 res = append(res, session)
142 }
143 sorted := sortedSessions(res)
144 sort.Sort(sorted)
145 res = []Session(sorted)
146 return res, nil
147 }
149 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
150 func RegisterSessionHandlers(r *mux.Router, context Context) {
151 r.Handle("/login", wrap(context, CreateSessionHandler))
152 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
153 // BUG(paddy): We need to implement a handler for terminating sessions.
154 }
156 func checkCSRF(r *http.Request, s Session) error {
157 if r.PostFormValue("csrftoken") != s.CSRFToken {
158 return ErrCSRFAttempt
159 }
160 return nil
161 }
163 func checkCookie(r *http.Request, context Context) (Session, error) {
164 cookie, err := r.Cookie(authCookieName)
165 if err == http.ErrNoCookie {
166 return Session{}, ErrNoSession
167 } else if err != nil {
168 log.Println(err)
169 return Session{}, err
170 }
171 sess, err := context.GetSession(cookie.Value)
172 if err == ErrSessionNotFound {
173 return Session{}, ErrInvalidSession
174 } else if err != nil {
175 return Session{}, err
176 }
177 if !sess.Active {
178 return Session{}, ErrInvalidSession
179 }
180 if time.Now().After(sess.Expires) {
181 return Session{}, ErrInvalidSession
182 }
183 return sess, nil
184 }
186 func buildLoginRedirect(r *http.Request, context Context) string {
187 if context.loginURI == nil {
188 return ""
189 }
190 uri := *context.loginURI
191 q := uri.Query()
192 q.Set("from", r.URL.String())
193 uri.RawQuery = q.Encode()
194 return uri.String()
195 }
197 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
198 realPass, err := hex.DecodeString(profile.Passphrase)
199 if err != nil {
200 return false, err
201 }
202 realSalt, err := hex.DecodeString(profile.Salt)
203 if err != nil {
204 return false, err
205 }
206 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
207 if !pass.Compare(candidate, realPass) {
208 return false, ErrIncorrectAuth
209 }
210 return true, nil
211 }
213 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
214 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
215 if err != nil {
216 return "", "", err
217 }
218 result = hex.EncodeToString(passBytes)
219 salt = hex.EncodeToString(saltBytes)
220 return result, salt, err
221 }
223 func pbkdf2sha256calc() (int, error) {
224 return pass.CalculateIterations(sha256.New)
225 }
227 func isAuthError(err error) bool {
228 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
229 }
231 func authenticate(user, passphrase string, context Context) (Profile, error) {
232 profile, err := context.GetProfileByLogin(user)
233 if err != nil {
234 if err == ErrProfileNotFound || err == ErrLoginNotFound {
235 return Profile{}, ErrIncorrectAuth
236 }
237 return Profile{}, err
238 }
239 if profile.Compromised {
240 return Profile{}, ErrProfileCompromised
241 }
242 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
243 return profile, ErrProfileLocked
244 }
245 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
246 if !ok {
247 return Profile{}, ErrInvalidPassphraseScheme
248 }
249 result, err := scheme.check(profile, passphrase)
250 if !result {
251 return Profile{}, err
252 }
253 return profile, nil
254 }
256 // CreateSessionHandler allows the user to log into their account and create their session.
257 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
258 errors := []error{}
259 if r.Method == "POST" {
260 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
261 if err == nil {
262 ip := r.Header.Get("X-Forwarded-For")
263 if ip == "" {
264 ip = r.RemoteAddr
265 }
266 sessionID := make([]byte, 32)
267 csrfToken := make([]byte, 32)
268 _, err = rand.Read(sessionID)
269 if err != nil {
270 log.Println("Error reading CSPRNG for session ID:", err)
271 w.WriteHeader(http.StatusInternalServerError)
272 w.Write([]byte("Internal error"))
273 return
274 }
275 _, err = rand.Read(csrfToken)
276 if err != nil {
277 log.Println("Error reading CSPRNG for CSRF token:", err)
278 w.WriteHeader(http.StatusInternalServerError)
279 w.Write([]byte("internal error"))
280 return
281 }
282 session := Session{
283 ID: base64.StdEncoding.EncodeToString(sessionID),
284 IP: ip,
285 UserAgent: r.UserAgent(),
286 ProfileID: profile.ID,
287 Login: r.PostFormValue("login"),
288 Created: time.Now(),
289 Expires: time.Now().Add(time.Hour),
290 Active: true,
291 CSRFToken: base64.StdEncoding.EncodeToString(csrfToken),
292 }
293 err = context.CreateSession(session)
294 if err != nil {
295 w.WriteHeader(http.StatusInternalServerError)
296 w.Write([]byte(err.Error()))
297 return
298 }
299 // BUG(paddy): We really need to do a security audit on our cookies.
300 cookie := http.Cookie{
301 Name: authCookieName,
302 Value: session.ID,
303 Expires: session.Expires,
304 HttpOnly: true,
305 Secure: context.config.secureCookie,
306 }
307 http.SetCookie(w, &cookie)
308 redirectTo := r.URL.Query().Get("from")
309 if redirectTo == "" {
310 redirectTo = "/"
311 }
312 http.Redirect(w, r, redirectTo, http.StatusFound)
313 return
314 } else if !isAuthError(err) {
315 w.WriteHeader(http.StatusInternalServerError)
316 w.Write([]byte(err.Error()))
317 return
318 } else {
319 errors = append(errors, err)
320 }
321 }
322 context.Render(w, loginTemplateName, map[string]interface{}{
323 "errors": errors,
324 })
325 }
327 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
328 enc := json.NewEncoder(w)
329 username := r.PostFormValue("username")
330 password := r.PostFormValue("password")
331 scopes = strings.Split(r.PostFormValue("scope"), " ")
332 profile, err := authenticate(username, password, context)
333 if err != nil {
334 if isAuthError(err) {
335 w.WriteHeader(http.StatusBadRequest)
336 renderJSONError(enc, "invalid_grant")
337 return
338 }
339 w.WriteHeader(http.StatusInternalServerError)
340 w.Write([]byte(err.Error()))
341 return
342 }
343 profileID = profile.ID
344 valid = true
345 return
346 }
348 func credentialsAuditString(r *http.Request) string {
349 return "credentials"
350 }