auth

Paddy 2014-12-14 Parent:0b45e6b9cb94 Child:c03b5eb3179e

104:bc77a315f823 Go to Latest

auth/session.go

Add request helpers. Fix a typo in the requestErrConflict constant. Create a response type that is used to build responses before sending them down the wire. Create vars for a few common types of error responses that never change. Create a negotiate middleware function that will respond with a 406 error if the client requests an encoding that we can't support. Create an encode helper that determines the requested encoding and uses it to send the data down the wire. Move our wrap middleware from the oauth2.go file to the request.go file, where it's more likely to be looked for.

History
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 string, 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 realSalt, err := hex.DecodeString(profile.Salt)
173 if err != nil {
174 return false, err
175 }
176 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
177 if !pass.Compare(candidate, realPass) {
178 return false, ErrIncorrectAuth
179 }
180 return true, nil
181 }
183 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
184 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
185 if err != nil {
186 return "", "", err
187 }
188 result = hex.EncodeToString(passBytes)
189 salt = hex.EncodeToString(saltBytes)
190 return result, salt, err
191 }
193 func pbkdf2sha256calc() (int, error) {
194 return pass.CalculateIterations(sha256.New)
195 }
197 func authenticate(user, passphrase string, context Context) (Profile, error) {
198 profile, err := context.GetProfileByLogin(user)
199 if err != nil {
200 if err == ErrProfileNotFound || err == ErrLoginNotFound {
201 return Profile{}, ErrIncorrectAuth
202 }
203 return Profile{}, err
204 }
205 if profile.Compromised {
206 return Profile{}, ErrProfileCompromised
207 }
208 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
209 return profile, ErrProfileLocked
210 }
211 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
212 if !ok {
213 return Profile{}, ErrInvalidPassphraseScheme
214 }
215 result, err := scheme.check(profile, passphrase)
216 if !result {
217 return Profile{}, err
218 }
219 return profile, nil
220 }
222 // CreateSessionHandler allows the user to log into their account and create their session.
223 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
224 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit
225 errors := []error{}
226 if r.Method == "POST" {
227 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
228 if err == nil {
229 ip := r.Header.Get("X-Forwarded-For")
230 if ip == "" {
231 ip = r.RemoteAddr
232 }
233 session := Session{
234 ID: uuid.NewID().String(),
235 IP: ip,
236 UserAgent: r.UserAgent(),
237 ProfileID: profile.ID,
238 Login: r.PostFormValue("login"),
239 Created: time.Now(),
240 Active: true,
241 }
242 err = context.CreateSession(session)
243 if err != nil {
244 w.WriteHeader(http.StatusInternalServerError)
245 w.Write([]byte(err.Error()))
246 return
247 }
248 // BUG(paddy): really need to do a security audit on our cookie
249 cookie := http.Cookie{
250 Name: authCookieName,
251 Value: session.ID,
252 Expires: time.Now().Add(24 * 7 * time.Hour),
253 HttpOnly: true,
254 }
255 http.SetCookie(w, &cookie)
256 redirectTo := r.URL.Query().Get("from")
257 if redirectTo == "" {
258 redirectTo = "/"
259 }
260 http.Redirect(w, r, redirectTo, http.StatusFound)
261 return
262 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
263 w.WriteHeader(http.StatusInternalServerError)
264 w.Write([]byte(err.Error()))
265 return
266 } else {
267 errors = append(errors, err)
268 }
269 }
270 context.Render(w, loginTemplateName, map[string]interface{}{
271 "errors": errors,
272 })
273 }