auth

Paddy 2015-03-03 Parent:163ce22fa4c9 Child:026adb0c7fc4

135:d30a3a12d387 Go to Latest

auth/session.go

Attach our Scope type to AuthCodes and Tokens. When obtaining an AuthorizationCode or Token, attach a slice of strings, each one a Scope ID, instead of just attaching the encoded string the user passes in. This will allow us to change our Scope encoding down the line, and is more conceptually faithful. Also, if an authorization request is made with an invalid scope, return the invalid_scope error.

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