Add an endpoint to validate and register profiles.
Add a newProfileRequest object that defines the user-specified properties of a
new Profile.
Add a helper that validates a newProfileRequest and modifies it for
sanitization, mostly just removing leading and trailing whitespace.
Add MaxNameLength, MaxUsernameLength, and MaxEmailLength constants to hold the
maximum length for those properties.
Add errors to be returned when a users attempts to log in with a profile that is
compromised or locked.
Add the bare bones of a CreateProfileHandler that validates a profile
registration request adn uses it to create a Profile and at least one Login.
Create a requestError struct that is used for returning API errors, along with
constants for the slugs we'll use to signal those errors.
12 "code.secondbit.org/pass"
13 "code.secondbit.org/uuid"
15 "github.com/gorilla/mux"
19 loginTemplateName = "login"
23 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
24 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
25 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
26 ErrSessionNotFound = errors.New("session not found in sessionStore")
27 // ErrInvalidSession is returned when a Session is specified but is not valid.
28 ErrInvalidSession = errors.New("session is not valid")
29 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
30 ErrSessionAlreadyExists = errors.New("session already exists")
32 passphraseSchemes = map[int]passphraseScheme{
34 check: pbkdf2sha256check,
35 create: pbkdf2sha256create,
36 calculateIterations: pbkdf2sha256calc,
41 type passphraseScheme struct {
42 check func(profile Profile, passphrase string) (bool, error)
43 create func(passphrase string, iterations int) (result, salt []byte, err error)
44 calculateIterations func() (int, error)
47 // Session represents a user's authenticated session, associating it with a profile
48 // and some audit data.
59 type sortedSessions []Session
61 func (s sortedSessions) Len() int {
65 func (s sortedSessions) Less(i, j int) bool {
66 return s[i].Created.After(s[j].Created)
69 func (s sortedSessions) Swap(i, j int) {
70 s[i], s[j] = s[j], s[i]
73 type sessionStore interface {
74 createSession(session Session) error
75 getSession(id string) (Session, error)
76 removeSession(id string) error
77 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
80 func (m *memstore) createSession(session Session) error {
82 defer m.sessionLock.Unlock()
83 if _, ok := m.sessions[session.ID]; ok {
84 return ErrSessionAlreadyExists
86 m.sessions[session.ID] = session
90 func (m *memstore) getSession(id string) (Session, error) {
92 defer m.sessionLock.RUnlock()
93 if _, ok := m.sessions[id]; !ok {
94 return Session{}, ErrSessionNotFound
96 return m.sessions[id], nil
99 func (m *memstore) removeSession(id string) error {
101 defer m.sessionLock.Unlock()
102 if _, ok := m.sessions[id]; !ok {
103 return ErrSessionNotFound
105 delete(m.sessions, id)
109 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
110 m.sessionLock.RLock()
111 defer m.sessionLock.RUnlock()
113 for _, session := range m.sessions {
114 if int64(len(res)) >= num {
117 if profile != nil && !profile.Equal(session.ProfileID) {
120 if !before.IsZero() && session.Created.After(before) {
123 res = append(res, session)
125 sorted := sortedSessions(res)
127 res = []Session(sorted)
131 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
132 func RegisterSessionHandlers(r *mux.Router, context Context) {
133 r.Handle("/login", wrap(context, CreateSessionHandler))
136 func checkCookie(r *http.Request, context Context) (Session, error) {
137 cookie, err := r.Cookie(authCookieName)
138 if err == http.ErrNoCookie {
139 return Session{}, ErrNoSession
140 } else if err != nil {
142 return Session{}, err
144 sess, err := context.GetSession(cookie.Value)
145 if err == ErrSessionNotFound {
146 return Session{}, ErrInvalidSession
147 } else if err != nil {
148 return Session{}, err
151 return Session{}, ErrInvalidSession
156 func buildLoginRedirect(r *http.Request, context Context) string {
157 if context.loginURI == nil {
160 uri := *context.loginURI
162 q.Set("from", r.URL.String())
163 uri.RawQuery = q.Encode()
167 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
168 realPass, err := hex.DecodeString(profile.Passphrase)
172 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
173 if !pass.Compare(candidate, realPass) {
174 return false, ErrIncorrectAuth
179 func pbkdf2sha256create(passphrase string, iters int) (result, salt []byte, err error) {
180 return pass.Create(sha256.New, iters, []byte(passphrase))
183 func pbkdf2sha256calc() (int, error) {
184 return pass.CalculateIterations(sha256.New)
187 func authenticate(user, passphrase string, context Context) (Profile, error) {
188 profile, err := context.GetProfileByLogin(user)
190 if err == ErrProfileNotFound || err == ErrLoginNotFound {
191 return Profile{}, ErrIncorrectAuth
193 return Profile{}, err
195 if profile.Compromised {
196 return Profile{}, ErrProfileCompromised
198 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
199 return profile, ErrProfileLocked
201 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
203 return Profile{}, ErrInvalidPassphraseScheme
205 result, err := scheme.check(profile, passphrase)
207 return Profile{}, err
212 // CreateSessionHandler allows the user to log into their account and create their session.
213 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
214 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit
216 if r.Method == "POST" {
217 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
219 ip := r.Header.Get("X-Forwarded-For")
224 ID: uuid.NewID().String(),
226 UserAgent: r.UserAgent(),
227 ProfileID: profile.ID,
228 Login: r.PostFormValue("login"),
232 err = context.CreateSession(session)
234 w.WriteHeader(http.StatusInternalServerError)
235 w.Write([]byte(err.Error()))
238 // BUG(paddy): really need to do a security audit on our cookie
239 cookie := http.Cookie{
240 Name: authCookieName,
242 Expires: time.Now().Add(24 * 7 * time.Hour),
245 http.SetCookie(w, &cookie)
246 redirectTo := r.URL.Query().Get("from")
247 if redirectTo == "" {
250 http.Redirect(w, r, redirectTo, http.StatusFound)
252 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
253 w.WriteHeader(http.StatusInternalServerError)
254 w.Write([]byte(err.Error()))
257 errors = append(errors, err)
260 context.Render(w, loginTemplateName, map[string]interface{}{