Break out scopes and events.
This repo has gotten unwieldy, and there are portions of it that need to be
imported by a large number of other packages.
For example, scopes will be used in almost every API we write. Rather than
importing the entirety of this codebase into every API we write, I've opted to
move the scope logic out into a scopes package, with a subpackage for the
defined types, which is all most projects actually want to import.
We also define some event type constants, and importing those shouldn't require
a project to import all our dependencies, either. So I made an events subpackage
that just holds those constants.
This package has become a little bit of a red-headed stepchild and is do for a
refactor, but I'm trying to put that off as long as I can.
The refactoring of our scopes stuff has left a bug wherein a token can be
granted for scopes that don't exist. I'm going to need to revisit that, and also
how to limit scopes to only be granted to the users that should be able to
request them. But that's a battle for another day.
16 "code.secondbit.org/pass.hg"
17 "code.secondbit.org/scopes.hg/types"
18 "code.secondbit.org/uuid.hg"
19 "github.com/gorilla/mux"
23 authCookieName = "auth"
24 loginTemplateName = "login"
28 RegisterGrantType("password", GrantType{
29 Validate: credentialsValidate,
32 ReturnToken: RenderJSONToken,
33 AuditString: credentialsAuditString,
38 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
39 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
40 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
41 ErrSessionNotFound = errors.New("session not found in sessionStore")
42 // ErrInvalidSession is returned when a Session is specified but is not valid.
43 ErrInvalidSession = errors.New("session is not valid")
44 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
45 ErrSessionAlreadyExists = errors.New("session already exists")
46 // ErrCSRFAttempt is returned when a CSRF attempt is detected.
47 ErrCSRFAttempt = errors.New("CSRF attempt")
49 passphraseSchemes = map[int]passphraseScheme{
51 check: pbkdf2sha256check,
52 create: pbkdf2sha256create,
53 calculateIterations: pbkdf2sha256calc,
58 type passphraseScheme struct {
59 check func(profile Profile, passphrase string) (bool, error)
60 create func(passphrase string, iterations int) (result, salt string, err error)
61 calculateIterations func() (int, error)
64 // Session represents a user's authenticated session, associating it with a profile
65 // and some audit data.
78 type sortedSessions []Session
80 func (s sortedSessions) Len() int {
84 func (s sortedSessions) Less(i, j int) bool {
85 return s[i].Created.After(s[j].Created)
88 func (s sortedSessions) Swap(i, j int) {
89 s[i], s[j] = s[j], s[i]
92 type sessionStore interface {
93 createSession(session Session) error
94 getSession(id string) (Session, error)
95 terminateSession(id string) error
96 removeSession(id string) error
97 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
98 terminateSessionsByProfile(profile uuid.ID) error
101 func (m *memstore) createSession(session Session) error {
103 defer m.sessionLock.Unlock()
104 if _, ok := m.sessions[session.ID]; ok {
105 return ErrSessionAlreadyExists
107 m.sessions[session.ID] = session
111 func (m *memstore) getSession(id string) (Session, error) {
112 m.sessionLock.RLock()
113 defer m.sessionLock.RUnlock()
114 if _, ok := m.sessions[id]; !ok {
115 return Session{}, ErrSessionNotFound
117 return m.sessions[id], nil
120 func (m *memstore) terminateSession(id string) error {
121 m.sessionLock.RLock()
122 defer m.sessionLock.RUnlock()
123 sess, ok := m.sessions[id]
125 return ErrSessionNotFound
128 m.sessions[id] = sess
132 func (m *memstore) terminateSessionsByProfile(profile uuid.ID) error {
133 m.sessionLock.RLock()
134 defer m.sessionLock.RUnlock()
136 for _, session := range m.sessions {
137 if profile.Equal(session.ProfileID) {
138 session.Active = false
139 m.sessions[session.ID] = session
144 return ErrProfileNotFound
149 func (m *memstore) removeSession(id string) error {
151 defer m.sessionLock.Unlock()
152 if _, ok := m.sessions[id]; !ok {
153 return ErrSessionNotFound
155 delete(m.sessions, id)
159 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
160 m.sessionLock.RLock()
161 defer m.sessionLock.RUnlock()
163 for _, session := range m.sessions {
164 if int64(len(res)) >= num {
167 if profile != nil && !profile.Equal(session.ProfileID) {
170 if !before.IsZero() && session.Created.After(before) {
173 res = append(res, session)
175 sorted := sortedSessions(res)
177 res = []Session(sorted)
181 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
182 func RegisterSessionHandlers(r *mux.Router, context Context) {
183 r.Handle("/login", wrap(context, CreateSessionHandler))
184 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
185 r.Handle("/sessions/{id}", wrap(context, TerminateSessionHandler)).Methods("OPTIONS", "DELETE")
188 func checkCSRF(r *http.Request, s Session) error {
189 if r.PostFormValue("csrftoken") != s.CSRFToken {
190 return ErrCSRFAttempt
195 func checkCookie(r *http.Request, context Context) (Session, error) {
196 cookie, err := r.Cookie(authCookieName)
197 if err == http.ErrNoCookie {
198 return Session{}, ErrNoSession
199 } else if err != nil {
201 return Session{}, err
203 sess, err := context.GetSession(cookie.Value)
204 if err == ErrSessionNotFound {
205 return Session{}, ErrInvalidSession
206 } else if err != nil {
207 return Session{}, err
210 return Session{}, ErrInvalidSession
212 if time.Now().After(sess.Expires) {
213 return Session{}, ErrInvalidSession
218 func buildLoginRedirect(r *http.Request, context Context) string {
219 if context.loginURI == nil {
222 uri := *context.loginURI
224 q.Set("from", r.URL.String())
225 uri.RawQuery = q.Encode()
229 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
230 realPass, err := hex.DecodeString(profile.Passphrase)
234 realSalt, err := hex.DecodeString(profile.Salt)
238 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
239 if !pass.Compare(candidate, realPass) {
240 return false, ErrIncorrectAuth
245 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
246 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
250 result = hex.EncodeToString(passBytes)
251 salt = hex.EncodeToString(saltBytes)
252 return result, salt, err
255 func pbkdf2sha256calc() (int, error) {
256 return pass.CalculateIterations(sha256.New)
259 func isAuthError(err error) bool {
260 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
263 func authenticate(user, passphrase string, context Context) (Profile, error) {
264 profile, err := context.GetProfileByLogin(user)
266 if err == ErrProfileNotFound || err == ErrLoginNotFound {
267 return Profile{}, ErrIncorrectAuth
269 return Profile{}, err
271 if profile.Compromised {
272 return Profile{}, ErrProfileCompromised
274 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
275 return profile, ErrProfileLocked
277 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
279 return Profile{}, ErrInvalidPassphraseScheme
281 result, err := scheme.check(profile, passphrase)
283 return Profile{}, err
288 // CreateSessionHandler allows the user to log into their account and create their session.
289 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
291 if r.Method == "POST" {
292 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
294 ip := r.Header.Get("X-Forwarded-For")
298 sessionID := make([]byte, 32)
299 csrfToken := make([]byte, 32)
300 _, err = rand.Read(sessionID)
302 log.Println("Error reading CSPRNG for session ID:", err)
303 w.WriteHeader(http.StatusInternalServerError)
304 w.Write([]byte("Internal error"))
307 _, err = rand.Read(csrfToken)
309 log.Println("Error reading CSPRNG for CSRF token:", err)
310 w.WriteHeader(http.StatusInternalServerError)
311 w.Write([]byte("internal error"))
315 ID: base64.URLEncoding.EncodeToString(sessionID),
317 UserAgent: r.UserAgent(),
318 ProfileID: profile.ID,
319 Login: r.PostFormValue("login"),
321 Expires: time.Now().Add(time.Hour),
323 CSRFToken: base64.URLEncoding.EncodeToString(csrfToken),
325 err = context.CreateSession(session)
327 w.WriteHeader(http.StatusInternalServerError)
328 w.Write([]byte(err.Error()))
331 // BUG(paddy): We really need to do a security audit on our cookies.
332 cookie := http.Cookie{
333 Name: authCookieName,
335 Expires: session.Expires,
337 Secure: context.config.secureCookie,
339 http.SetCookie(w, &cookie)
340 redirectTo := r.URL.Query().Get("from")
341 if redirectTo == "" {
344 http.Redirect(w, r, redirectTo, http.StatusFound)
346 } else if !isAuthError(err) {
347 w.WriteHeader(http.StatusInternalServerError)
348 w.Write([]byte(err.Error()))
351 errors = append(errors, err)
354 context.Render(w, loginTemplateName, map[string]interface{}{
359 // TerminateSessionHandler allows the user to end their session before it expires.
360 func TerminateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
361 var errors []RequestError
363 if vars["id"] == "" {
364 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
365 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
369 un, pw, ok := r.BasicAuth()
371 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
372 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
375 profile, err := authenticate(un, pw, context)
377 if isAuthError(err) {
378 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
379 encode(w, r, http.StatusForbidden, Response{Errors: errors})
382 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
383 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
386 session, err := context.GetSession(id)
388 if err == ErrSessionNotFound {
389 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
390 encode(w, r, http.StatusNotFound, Response{Errors: errors})
393 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
394 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
397 if !session.ProfileID.Equal(profile.ID) {
398 errors = append(errors, RequestError{Slug: RequestErrAccessDenied, Param: "id"})
399 encode(w, r, http.StatusForbidden, Response{Errors: errors})
402 err = context.TerminateSession(id)
404 if err == ErrSessionNotFound {
405 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
406 encode(w, r, http.StatusNotFound, Response{Errors: errors})
409 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
410 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
413 session.Active = false
414 encode(w, r, http.StatusOK, Response{Sessions: []Session{session}, Errors: errors})
417 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes scopeTypes.Scopes, profileID uuid.ID, valid bool) {
418 enc := json.NewEncoder(w)
419 username := r.PostFormValue("username")
420 password := r.PostFormValue("password")
421 scopes = scopeTypes.StringsToScopes(strings.Split(r.PostFormValue("scope"), " "))
422 profile, err := authenticate(username, password, context)
424 if isAuthError(err) {
425 w.WriteHeader(http.StatusBadRequest)
426 renderJSONError(enc, "invalid_grant")
429 w.WriteHeader(http.StatusInternalServerError)
430 w.Write([]byte(err.Error()))
433 profileID = profile.ID
438 func credentialsAuditString(r *http.Request) string {