Implement postgres version of the tokenStore.
Create a postgres implementation for the tokenStore. Note that because pq
doesn't support Postgres' array types (see https://github.com/lib/pq/issues/49),
we couldn't just store the token.Scopes field as a Postgres array of varchars,
which would have been the ideal. Instead, we need a many-to-many table that maps
tokens to scopes. This meant we needed a special tokenScope type for our
database mapping, and we needed to complicate the token storage/retrieval
functions a little bit. It's kind of ugly, I'm not a huge fan of it, and I'd
much rather be using the Postgres array types, but... well, here we are.
We also added the postgres tokenStore to our slice of tokenStores to test when
the correct environment variables are present.
We wrote initialization SQL for the tables required by the postgres tokenStore.
Also, added a helper script for emptying the test database, because I got tired
of doing it by hand. We should be doing that in an automated fashion in the
tests themselves, but that would mean extending the *Store interfaces.
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 removeSession(id string) error
95 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
98 func (m *memstore) createSession(session Session) error {
100 defer m.sessionLock.Unlock()
101 if _, ok := m.sessions[session.ID]; ok {
102 return ErrSessionAlreadyExists
104 m.sessions[session.ID] = session
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
114 return m.sessions[id], nil
117 func (m *memstore) removeSession(id string) error {
119 defer m.sessionLock.Unlock()
120 if _, ok := m.sessions[id]; !ok {
121 return ErrSessionNotFound
123 delete(m.sessions, id)
127 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
128 m.sessionLock.RLock()
129 defer m.sessionLock.RUnlock()
131 for _, session := range m.sessions {
132 if int64(len(res)) >= num {
135 if profile != nil && !profile.Equal(session.ProfileID) {
138 if !before.IsZero() && session.Created.After(before) {
141 res = append(res, session)
143 sorted := sortedSessions(res)
145 res = []Session(sorted)
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.
156 func checkCSRF(r *http.Request, s Session) error {
157 if r.PostFormValue("csrftoken") != s.CSRFToken {
158 return ErrCSRFAttempt
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 {
169 return Session{}, err
171 sess, err := context.GetSession(cookie.Value)
172 if err == ErrSessionNotFound {
173 return Session{}, ErrInvalidSession
174 } else if err != nil {
175 return Session{}, err
178 return Session{}, ErrInvalidSession
180 if time.Now().After(sess.Expires) {
181 return Session{}, ErrInvalidSession
186 func buildLoginRedirect(r *http.Request, context Context) string {
187 if context.loginURI == nil {
190 uri := *context.loginURI
192 q.Set("from", r.URL.String())
193 uri.RawQuery = q.Encode()
197 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
198 realPass, err := hex.DecodeString(profile.Passphrase)
202 realSalt, err := hex.DecodeString(profile.Salt)
206 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
207 if !pass.Compare(candidate, realPass) {
208 return false, ErrIncorrectAuth
213 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
214 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
218 result = hex.EncodeToString(passBytes)
219 salt = hex.EncodeToString(saltBytes)
220 return result, salt, err
223 func pbkdf2sha256calc() (int, error) {
224 return pass.CalculateIterations(sha256.New)
227 func isAuthError(err error) bool {
228 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
231 func authenticate(user, passphrase string, context Context) (Profile, error) {
232 profile, err := context.GetProfileByLogin(user)
234 if err == ErrProfileNotFound || err == ErrLoginNotFound {
235 return Profile{}, ErrIncorrectAuth
237 return Profile{}, err
239 if profile.Compromised {
240 return Profile{}, ErrProfileCompromised
242 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
243 return profile, ErrProfileLocked
245 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
247 return Profile{}, ErrInvalidPassphraseScheme
249 result, err := scheme.check(profile, passphrase)
251 return Profile{}, err
256 // CreateSessionHandler allows the user to log into their account and create their session.
257 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
259 if r.Method == "POST" {
260 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
262 ip := r.Header.Get("X-Forwarded-For")
266 sessionID := make([]byte, 32)
267 csrfToken := make([]byte, 32)
268 _, err = rand.Read(sessionID)
270 log.Println("Error reading CSPRNG for session ID:", err)
271 w.WriteHeader(http.StatusInternalServerError)
272 w.Write([]byte("Internal error"))
275 _, err = rand.Read(csrfToken)
277 log.Println("Error reading CSPRNG for CSRF token:", err)
278 w.WriteHeader(http.StatusInternalServerError)
279 w.Write([]byte("internal error"))
283 ID: base64.StdEncoding.EncodeToString(sessionID),
285 UserAgent: r.UserAgent(),
286 ProfileID: profile.ID,
287 Login: r.PostFormValue("login"),
289 Expires: time.Now().Add(time.Hour),
291 CSRFToken: base64.StdEncoding.EncodeToString(csrfToken),
293 err = context.CreateSession(session)
295 w.WriteHeader(http.StatusInternalServerError)
296 w.Write([]byte(err.Error()))
299 // BUG(paddy): We really need to do a security audit on our cookies.
300 cookie := http.Cookie{
301 Name: authCookieName,
303 Expires: session.Expires,
305 Secure: context.config.secureCookie,
307 http.SetCookie(w, &cookie)
308 redirectTo := r.URL.Query().Get("from")
309 if redirectTo == "" {
312 http.Redirect(w, r, redirectTo, http.StatusFound)
314 } else if !isAuthError(err) {
315 w.WriteHeader(http.StatusInternalServerError)
316 w.Write([]byte(err.Error()))
319 errors = append(errors, err)
322 context.Render(w, loginTemplateName, map[string]interface{}{
327 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
328 enc := json.NewEncoder(w)
329 username := r.PostFormValue("username")
330 password := r.PostFormValue("password")
331 scopes = strings.Split(r.PostFormValue("scope"), " ")
332 profile, err := authenticate(username, password, context)
334 if isAuthError(err) {
335 w.WriteHeader(http.StatusBadRequest)
336 renderJSONError(enc, "invalid_grant")
339 w.WriteHeader(http.StatusInternalServerError)
340 w.Write([]byte(err.Error()))
343 profileID = profile.ID
348 func credentialsAuditString(r *http.Request) string {