auth
auth/session.go
Change normalization flags to a constant. Let's use a constant so we can ensure we're using the same flags everywhere. Otherwise, we can get weird data corruption because we use the wrong flags.
1 package auth
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "log"
9 "net/http"
10 "sort"
11 "time"
13 "code.secondbit.org/pass.hg"
14 "code.secondbit.org/uuid.hg"
15 "github.com/gorilla/mux"
16 )
18 const (
19 loginTemplateName = "login"
20 )
22 func init() {
23 RegisterGrantType("password", GrantType{
24 Validate: credentialsValidate,
25 Invalidate: nil,
26 IssuesRefresh: true,
27 ReturnToken: RenderJSONToken,
28 AuditString: credentialsAuditString,
29 })
30 }
32 var (
33 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
34 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
35 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
36 ErrSessionNotFound = errors.New("session not found in sessionStore")
37 // ErrInvalidSession is returned when a Session is specified but is not valid.
38 ErrInvalidSession = errors.New("session is not valid")
39 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
40 ErrSessionAlreadyExists = errors.New("session already exists")
42 passphraseSchemes = map[int]passphraseScheme{
43 1: {
44 check: pbkdf2sha256check,
45 create: pbkdf2sha256create,
46 calculateIterations: pbkdf2sha256calc,
47 },
48 }
49 )
51 type passphraseScheme struct {
52 check func(profile Profile, passphrase string) (bool, error)
53 create func(passphrase string, iterations int) (result, salt string, err error)
54 calculateIterations func() (int, error)
55 }
57 // Session represents a user's authenticated session, associating it with a profile
58 // and some audit data.
59 type Session struct {
60 ID string
61 IP string
62 UserAgent string
63 ProfileID uuid.ID
64 Login string
65 Created time.Time
66 Active bool
67 }
69 type sortedSessions []Session
71 func (s sortedSessions) Len() int {
72 return len(s)
73 }
75 func (s sortedSessions) Less(i, j int) bool {
76 return s[i].Created.After(s[j].Created)
77 }
79 func (s sortedSessions) Swap(i, j int) {
80 s[i], s[j] = s[j], s[i]
81 }
83 type sessionStore interface {
84 createSession(session Session) error
85 getSession(id string) (Session, error)
86 removeSession(id string) error
87 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
88 }
90 func (m *memstore) createSession(session Session) error {
91 m.sessionLock.Lock()
92 defer m.sessionLock.Unlock()
93 if _, ok := m.sessions[session.ID]; ok {
94 return ErrSessionAlreadyExists
95 }
96 m.sessions[session.ID] = session
97 return nil
98 }
100 func (m *memstore) getSession(id string) (Session, error) {
101 m.sessionLock.RLock()
102 defer m.sessionLock.RUnlock()
103 if _, ok := m.sessions[id]; !ok {
104 return Session{}, ErrSessionNotFound
105 }
106 return m.sessions[id], nil
107 }
109 func (m *memstore) removeSession(id string) error {
110 m.sessionLock.Lock()
111 defer m.sessionLock.Unlock()
112 if _, ok := m.sessions[id]; !ok {
113 return ErrSessionNotFound
114 }
115 delete(m.sessions, id)
116 return nil
117 }
119 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
120 m.sessionLock.RLock()
121 defer m.sessionLock.RUnlock()
122 res := []Session{}
123 for _, session := range m.sessions {
124 if int64(len(res)) >= num {
125 break
126 }
127 if profile != nil && !profile.Equal(session.ProfileID) {
128 continue
129 }
130 if !before.IsZero() && session.Created.After(before) {
131 continue
132 }
133 res = append(res, session)
134 }
135 sorted := sortedSessions(res)
136 sort.Sort(sorted)
137 res = []Session(sorted)
138 return res, nil
139 }
141 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
142 func RegisterSessionHandlers(r *mux.Router, context Context) {
143 r.Handle("/login", wrap(context, CreateSessionHandler))
144 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
145 // BUG(paddy): We need to implement a handler for terminating sessions.
146 }
148 func checkCookie(r *http.Request, context Context) (Session, error) {
149 cookie, err := r.Cookie(authCookieName)
150 if err == http.ErrNoCookie {
151 return Session{}, ErrNoSession
152 } else if err != nil {
153 log.Println(err)
154 return Session{}, err
155 }
156 sess, err := context.GetSession(cookie.Value)
157 if err == ErrSessionNotFound {
158 return Session{}, ErrInvalidSession
159 } else if err != nil {
160 return Session{}, err
161 }
162 if !sess.Active {
163 return Session{}, ErrInvalidSession
164 }
165 return sess, nil
166 }
168 func buildLoginRedirect(r *http.Request, context Context) string {
169 if context.loginURI == nil {
170 return ""
171 }
172 uri := *context.loginURI
173 q := uri.Query()
174 q.Set("from", r.URL.String())
175 uri.RawQuery = q.Encode()
176 return uri.String()
177 }
179 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
180 realPass, err := hex.DecodeString(profile.Passphrase)
181 if err != nil {
182 return false, err
183 }
184 realSalt, err := hex.DecodeString(profile.Salt)
185 if err != nil {
186 return false, err
187 }
188 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
189 if !pass.Compare(candidate, realPass) {
190 return false, ErrIncorrectAuth
191 }
192 return true, nil
193 }
195 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
196 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
197 if err != nil {
198 return "", "", err
199 }
200 result = hex.EncodeToString(passBytes)
201 salt = hex.EncodeToString(saltBytes)
202 return result, salt, err
203 }
205 func pbkdf2sha256calc() (int, error) {
206 return pass.CalculateIterations(sha256.New)
207 }
209 func authenticate(user, passphrase string, context Context) (Profile, error) {
210 profile, err := context.GetProfileByLogin(user)
211 if err != nil {
212 if err == ErrProfileNotFound || err == ErrLoginNotFound {
213 return Profile{}, ErrIncorrectAuth
214 }
215 return Profile{}, err
216 }
217 if profile.Compromised {
218 return Profile{}, ErrProfileCompromised
219 }
220 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
221 return profile, ErrProfileLocked
222 }
223 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
224 if !ok {
225 return Profile{}, ErrInvalidPassphraseScheme
226 }
227 result, err := scheme.check(profile, passphrase)
228 if !result {
229 return Profile{}, err
230 }
231 return profile, nil
232 }
234 // CreateSessionHandler allows the user to log into their account and create their session.
235 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
236 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit
237 errors := []error{}
238 if r.Method == "POST" {
239 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
240 if err == nil {
241 ip := r.Header.Get("X-Forwarded-For")
242 if ip == "" {
243 ip = r.RemoteAddr
244 }
245 session := Session{
246 ID: uuid.NewID().String(),
247 IP: ip,
248 UserAgent: r.UserAgent(),
249 ProfileID: profile.ID,
250 Login: r.PostFormValue("login"),
251 Created: time.Now(),
252 Active: true,
253 }
254 err = context.CreateSession(session)
255 if err != nil {
256 w.WriteHeader(http.StatusInternalServerError)
257 w.Write([]byte(err.Error()))
258 return
259 }
260 // BUG(paddy): We really need to do a security audit on our cookie.
261 cookie := http.Cookie{
262 Name: authCookieName,
263 Value: session.ID,
264 Expires: time.Now().Add(24 * 7 * time.Hour),
265 HttpOnly: true,
266 }
267 http.SetCookie(w, &cookie)
268 redirectTo := r.URL.Query().Get("from")
269 if redirectTo == "" {
270 redirectTo = "/"
271 }
272 http.Redirect(w, r, redirectTo, http.StatusFound)
273 return
274 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
275 w.WriteHeader(http.StatusInternalServerError)
276 w.Write([]byte(err.Error()))
277 return
278 } else {
279 errors = append(errors, err)
280 }
281 }
282 context.Render(w, loginTemplateName, map[string]interface{}{
283 "errors": errors,
284 })
285 }
287 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
288 enc := json.NewEncoder(w)
289 username := r.PostFormValue("username")
290 password := r.PostFormValue("password")
291 scope = r.PostFormValue("scope")
292 profile, err := authenticate(username, password, context)
293 if err != nil {
294 if err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked {
295 w.WriteHeader(http.StatusBadRequest)
296 renderJSONError(enc, "invalid_grant")
297 return
298 }
299 w.WriteHeader(http.StatusInternalServerError)
300 w.Write([]byte(err.Error()))
301 return
302 }
303 profileID = profile.ID
304 valid = true
305 return
306 }
308 func credentialsAuditString(r *http.Request) string {
309 return "credentials"
310 }