Clean up after Client deletion, finish cleaning up after Profile deletion.
6f473576c6ae started cleaning up after Profiles when they're deleted, but
didn't clean up the Clients created by that Profile. This fixes that, and also
fixes a BUG note about cleaning up after a Client when it's deleted.
Extend the authorizationCodeStore to have a deleteAuthorizationCodesByClientID
method that will delete the AuthorizationCodes that have been granted by the
Client specified by the passed ID. We also implemented this in memstore and
postgres, so tests continue to pass.
Extend the clientStore to have a deleteClientsByOwner method that will delete
the Clients that were created by the Profile specified by the passed ID. We also
implemented this in memstore and postgres, so tests continue to pass.
Extend the clientStore to have a removeEndpointsByClientID method that will
delete the Endpoints that belong(ed) to a the Client specified by the passed ID.
We also implemented this in memstore and postgres, so tests continue to pass.
Extend the tokenStore to have a revokeTokensByClientID method that will revoke
all the Tokens that were granted to the Client specified by the passed ID. We
also implemented this in memstore and postgres, so tests continue to pass.
When listing Clients by their owner, allow setting the num argument (which
controls how many to return) to 0 or lower, and using that to signal "return all
Clients belonging to this owner", instead of paging. This is useful when
deleting the Clients belonging to a Profile as part of the cleanup after
deleting the Profile.
Create a cleanUpAfterClientDeletion helper function that will delete the
Endpoints and AuthorizationCodes belonging to a Client, and revoke the Tokens
belonging to a Client, as part of cleaning up after a Client has been deleted.
Add a check in the handler for listing Clients owned by a Profile to disallow
the num argument to be lower than 1, because the API should be forced to page.
Call our cleanUpAfterClientDeletion once the Client has been deleted in the
appropriate handler.
Fill out our Context with new methods to wrap all the new methods we're adding
to our *Stores.
In cleanUpAfterProfileDeletion, obtain a list of clients belonging to the owner,
use our new DeleteClientsByOwner method to remove all of them, and then use the
list to run our new cleanUpAfterClientDeletion function to clear away the final
remnants of a Profile when it's deleted.
16 "code.secondbit.org/pass.hg"
17 "code.secondbit.org/uuid.hg"
18 "github.com/gorilla/mux"
22 authCookieName = "auth"
23 loginTemplateName = "login"
27 RegisterGrantType("password", GrantType{
28 Validate: credentialsValidate,
31 ReturnToken: RenderJSONToken,
32 AuditString: credentialsAuditString,
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{
50 check: pbkdf2sha256check,
51 create: pbkdf2sha256create,
52 calculateIterations: pbkdf2sha256calc,
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)
63 // Session represents a user's authenticated session, associating it with a profile
64 // and some audit data.
77 type sortedSessions []Session
79 func (s sortedSessions) Len() int {
83 func (s sortedSessions) Less(i, j int) bool {
84 return s[i].Created.After(s[j].Created)
87 func (s sortedSessions) Swap(i, j int) {
88 s[i], s[j] = s[j], s[i]
91 type sessionStore interface {
92 createSession(session Session) error
93 getSession(id string) (Session, error)
94 terminateSession(id string) error
95 removeSession(id string) error
96 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
97 terminateSessionsByProfile(profile uuid.ID) error
100 func (m *memstore) createSession(session Session) error {
102 defer m.sessionLock.Unlock()
103 if _, ok := m.sessions[session.ID]; ok {
104 return ErrSessionAlreadyExists
106 m.sessions[session.ID] = session
110 func (m *memstore) getSession(id string) (Session, error) {
111 m.sessionLock.RLock()
112 defer m.sessionLock.RUnlock()
113 if _, ok := m.sessions[id]; !ok {
114 return Session{}, ErrSessionNotFound
116 return m.sessions[id], nil
119 func (m *memstore) terminateSession(id string) error {
120 m.sessionLock.RLock()
121 defer m.sessionLock.RUnlock()
122 sess, ok := m.sessions[id]
124 return ErrSessionNotFound
127 m.sessions[id] = sess
131 func (m *memstore) terminateSessionsByProfile(profile uuid.ID) error {
132 m.sessionLock.RLock()
133 defer m.sessionLock.RUnlock()
135 for _, session := range m.sessions {
136 if profile.Equal(session.ProfileID) {
137 session.Active = false
138 m.sessions[session.ID] = session
143 return ErrProfileNotFound
148 func (m *memstore) removeSession(id string) error {
150 defer m.sessionLock.Unlock()
151 if _, ok := m.sessions[id]; !ok {
152 return ErrSessionNotFound
154 delete(m.sessions, id)
158 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
159 m.sessionLock.RLock()
160 defer m.sessionLock.RUnlock()
162 for _, session := range m.sessions {
163 if int64(len(res)) >= num {
166 if profile != nil && !profile.Equal(session.ProfileID) {
169 if !before.IsZero() && session.Created.After(before) {
172 res = append(res, session)
174 sorted := sortedSessions(res)
176 res = []Session(sorted)
180 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
181 func RegisterSessionHandlers(r *mux.Router, context Context) {
182 r.Handle("/login", wrap(context, CreateSessionHandler))
183 // BUG(paddy): We need to implement a handler for listing sessions active on a profile.
184 r.Handle("/sessions/{id}", wrap(context, TerminateSessionHandler)).Methods("OPTIONS", "DELETE")
187 func checkCSRF(r *http.Request, s Session) error {
188 if r.PostFormValue("csrftoken") != s.CSRFToken {
189 return ErrCSRFAttempt
194 func checkCookie(r *http.Request, context Context) (Session, error) {
195 cookie, err := r.Cookie(authCookieName)
196 if err == http.ErrNoCookie {
197 return Session{}, ErrNoSession
198 } else if err != nil {
200 return Session{}, err
202 sess, err := context.GetSession(cookie.Value)
203 if err == ErrSessionNotFound {
204 return Session{}, ErrInvalidSession
205 } else if err != nil {
206 return Session{}, err
209 return Session{}, ErrInvalidSession
211 if time.Now().After(sess.Expires) {
212 return Session{}, ErrInvalidSession
217 func buildLoginRedirect(r *http.Request, context Context) string {
218 if context.loginURI == nil {
221 uri := *context.loginURI
223 q.Set("from", r.URL.String())
224 uri.RawQuery = q.Encode()
228 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
229 realPass, err := hex.DecodeString(profile.Passphrase)
233 realSalt, err := hex.DecodeString(profile.Salt)
237 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
238 if !pass.Compare(candidate, realPass) {
239 return false, ErrIncorrectAuth
244 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
245 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
249 result = hex.EncodeToString(passBytes)
250 salt = hex.EncodeToString(saltBytes)
251 return result, salt, err
254 func pbkdf2sha256calc() (int, error) {
255 return pass.CalculateIterations(sha256.New)
258 func isAuthError(err error) bool {
259 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
262 func authenticate(user, passphrase string, context Context) (Profile, error) {
263 profile, err := context.GetProfileByLogin(user)
265 if err == ErrProfileNotFound || err == ErrLoginNotFound {
266 return Profile{}, ErrIncorrectAuth
268 return Profile{}, err
270 if profile.Compromised {
271 return Profile{}, ErrProfileCompromised
273 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
274 return profile, ErrProfileLocked
276 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
278 return Profile{}, ErrInvalidPassphraseScheme
280 result, err := scheme.check(profile, passphrase)
282 return Profile{}, err
287 // CreateSessionHandler allows the user to log into their account and create their session.
288 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
290 if r.Method == "POST" {
291 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
293 ip := r.Header.Get("X-Forwarded-For")
297 sessionID := make([]byte, 32)
298 csrfToken := make([]byte, 32)
299 _, err = rand.Read(sessionID)
301 log.Println("Error reading CSPRNG for session ID:", err)
302 w.WriteHeader(http.StatusInternalServerError)
303 w.Write([]byte("Internal error"))
306 _, err = rand.Read(csrfToken)
308 log.Println("Error reading CSPRNG for CSRF token:", err)
309 w.WriteHeader(http.StatusInternalServerError)
310 w.Write([]byte("internal error"))
314 ID: base64.URLEncoding.EncodeToString(sessionID),
316 UserAgent: r.UserAgent(),
317 ProfileID: profile.ID,
318 Login: r.PostFormValue("login"),
320 Expires: time.Now().Add(time.Hour),
322 CSRFToken: base64.URLEncoding.EncodeToString(csrfToken),
324 err = context.CreateSession(session)
326 w.WriteHeader(http.StatusInternalServerError)
327 w.Write([]byte(err.Error()))
330 // BUG(paddy): We really need to do a security audit on our cookies.
331 cookie := http.Cookie{
332 Name: authCookieName,
334 Expires: session.Expires,
336 Secure: context.config.secureCookie,
338 http.SetCookie(w, &cookie)
339 redirectTo := r.URL.Query().Get("from")
340 if redirectTo == "" {
343 http.Redirect(w, r, redirectTo, http.StatusFound)
345 } else if !isAuthError(err) {
346 w.WriteHeader(http.StatusInternalServerError)
347 w.Write([]byte(err.Error()))
350 errors = append(errors, err)
353 context.Render(w, loginTemplateName, map[string]interface{}{
358 // TerminateSessionHandler allows the user to end their session before it expires.
359 func TerminateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
360 var errors []requestError
362 if vars["id"] == "" {
363 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
364 encode(w, r, http.StatusBadRequest, response{Errors: errors})
368 un, pw, ok := r.BasicAuth()
370 errors = append(errors, requestError{Slug: requestErrAccessDenied})
371 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
374 profile, err := authenticate(un, pw, context)
376 if isAuthError(err) {
377 errors = append(errors, requestError{Slug: requestErrAccessDenied})
378 encode(w, r, http.StatusForbidden, response{Errors: errors})
381 errors = append(errors, requestError{Slug: requestErrActOfGod})
382 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
385 session, err := context.GetSession(id)
387 if err == ErrSessionNotFound {
388 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
389 encode(w, r, http.StatusNotFound, response{Errors: errors})
392 errors = append(errors, requestError{Slug: requestErrActOfGod})
393 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
396 if !session.ProfileID.Equal(profile.ID) {
397 errors = append(errors, requestError{Slug: requestErrAccessDenied, Param: "id"})
398 encode(w, r, http.StatusForbidden, response{Errors: errors})
401 err = context.TerminateSession(id)
403 if err == ErrSessionNotFound {
404 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
405 encode(w, r, http.StatusNotFound, response{Errors: errors})
408 errors = append(errors, requestError{Slug: requestErrActOfGod})
409 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
412 session.Active = false
413 encode(w, r, http.StatusOK, response{Sessions: []Session{session}, Errors: errors})
416 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
417 enc := json.NewEncoder(w)
418 username := r.PostFormValue("username")
419 password := r.PostFormValue("password")
420 scopes = stringsToScopes(strings.Split(r.PostFormValue("scope"), " "))
421 profile, err := authenticate(username, password, context)
423 if isAuthError(err) {
424 w.WriteHeader(http.StatusBadRequest)
425 renderJSONError(enc, "invalid_grant")
428 w.WriteHeader(http.StatusInternalServerError)
429 w.Write([]byte(err.Error()))
432 profileID = profile.ID
437 func credentialsAuditString(r *http.Request) string {