auth

Paddy 2014-12-14 Parent:09c47387e455 Child:0b45e6b9cb94

99:5bccbed6631b Go to Latest

auth/session.go

Add an endpoint to validate and register profiles. Add a newProfileRequest object that defines the user-specified properties of a new Profile. Add a helper that validates a newProfileRequest and modifies it for sanitization, mostly just removing leading and trailing whitespace. Add MaxNameLength, MaxUsernameLength, and MaxEmailLength constants to hold the maximum length for those properties. Add errors to be returned when a users attempts to log in with a profile that is compromised or locked. Add the bare bones of a CreateProfileHandler that validates a profile registration request adn uses it to create a Profile and at least one Login. Create a requestError struct that is used for returning API errors, along with constants for the slugs we'll use to signal those errors.

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