Implement postgres version of scopeStore.
Update the authd server to use postgres as its scopeStore, instead of memstore.
panic when starting the authd server if the CreateScopes call fails. This
should, ideally, ignore ErrScopeAlreadyExists errors, but does not as of this
commit.
Update the simple.gotmpl template to properly display scopes, after switching to
the Scope type instead of simply passing around the string the client supplied
broke the template and I never bothered fixing it.
Update the updateScopes method on the scopeStore (and the corresponding
UpdateScopes method on the Context type) to be updateScope/UpdateScope.
Operating on several scopes at a time like that is simply too challenging in
SQL and I can't justify the complexity with a use case.
Add a helper method to ScopeChange called Empty(), which returns true if the
ScopeChange is full of nil values.
Remove the ID from the ScopeChange type, because we're no longer accepting
multiple ScopeChange types in UpdateScope, so we can supply that information
outside the ScopeChange, which matches the rest of our update* methods.
Correct our tests in scope_test.go to correctly use the updateScope method
instead of the old updateScopes method. This generally just resulted in calling
updateScope multiple times, as opposed to just once.
Add a scope table initialization to the sql/postgres_init.sql script.
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 {