auth

Paddy 2015-06-29 Parent:8ecb60d29b0d Child:b7e685839a1b

175:aa14e29b666f Go to Latest

auth/session.go

Create Docker image for authd. Create a Dockerfile for authd, which will wrap the compiled Go binary up into a tiny little Docker image. Create an authd/build-docker.sh script that will build the statically-linked binary in a Docker container, so the authd Docker image can use it. We had to include ca-certificates.crt in the Dockerfile, as well, so we could communicate over SSL with things. A wrapper.sh file is included that will pull the JWT_SECRET environment variable out of a kubernetes secrets file, which is a handy wrapper to have. Finally, we added the authd/docker-authd binary to the .hgignore.

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 "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 }