auth
auth/session.go
Store salts and passphrases as hex-encoded strings. Update our passphraseScheme.create function signature to return strings. Hex encode our passphrases and salts when encrypthing them so they're easier to store safely. Decode our salt before using it to check candidate passphrases.
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 }