auth

Paddy 2015-01-10 Parent:c03b5eb3179e Child:3ec7134fa211

114:385ac6294cdc Go to Latest

auth/session.go

Added tag rewrite_begins for changeset e0b3064daf02

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