Use postgres arrays for scope associations.
Use the new pqarrays library I wrote to store Scope associations for Tokens and
AuthorizationCodes, instead of using our hacky and abstraction-breaking
many-to-many code.
We also created the authStore.deleteAuthorizationCodesByProfileID method, to
clear out the AuthorizationCodes that belong to a Profile (used when the Profile
is deleted). So we added the implementation for memstore and for our postgres
store.
Call Context.DeleteAuthorizationCodesByProfileID when deleting a Profile to
clean up after it.
Rename sortedScopes to Scopes, which we use pqarrays.StringArray's methods on to
fulfill the sql.Scanner and driver.Valuer interfaces. This lets us store Scopes
in postgres arrays.
Create a stringsToScopes helper function that creates Scope objects, with their
IDs filled by the strings specified.
Update our GrantType.Validate function signature to return Scopes instead of
[]string.
Create a Scopes.Strings() helper method that returns a []string of the IDs of
the Scopes.
Update our SQL init file to use the new postgres array definition, instead of
the many-to-many definition.
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 {