Refactor verifyClient, implement refresh tokens.
Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient
out of each of the GrantType's validation functions and into the access token
endpoint, where it will be called before the GrantType's validation function.
Yay, less code repetition. And seeing as we always want to verify the client,
that seems like a good way to prevent things like 118a69954621 from happening.
This did, however, force us to add an AllowsPublic property to the GrantType, so
the token endpoint knows whether or not a public Client is valid for any given
GrantType.
We also implemented the refresh token grant type, which required adding ClientID
and RefreshRevoked as properties on the Token type. We need ClientID because we
need to constrain refresh tokens to the client that issued them. We also should
probably keep track of which tokens belong to which clients, just as a general
rule of thumb. RefreshRevoked had to be created, next to Revoked, because the
AccessToken could be revoked and the RefreshToken still valid, or vice versa.
Notably, when you issue a new refresh token, the old one is revoked, but the
access token is still valid. It remains to be seen whether this is a good way to
track things or not. The number of duplicated properties lead me to believe our
type is not a great representation of the underlying concepts.
13 "code.secondbit.org/pass.hg"
14 "code.secondbit.org/uuid.hg"
15 "github.com/gorilla/mux"
19 loginTemplateName = "login"
23 RegisterGrantType("password", GrantType{
24 Validate: credentialsValidate,
27 ReturnToken: RenderJSONToken,
32 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first.
33 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context")
34 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore.
35 ErrSessionNotFound = errors.New("session not found in sessionStore")
36 // ErrInvalidSession is returned when a Session is specified but is not valid.
37 ErrInvalidSession = errors.New("session is not valid")
38 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore.
39 ErrSessionAlreadyExists = errors.New("session already exists")
41 passphraseSchemes = map[int]passphraseScheme{
43 check: pbkdf2sha256check,
44 create: pbkdf2sha256create,
45 calculateIterations: pbkdf2sha256calc,
50 type passphraseScheme struct {
51 check func(profile Profile, passphrase string) (bool, error)
52 create func(passphrase string, iterations int) (result, salt string, err error)
53 calculateIterations func() (int, error)
56 // Session represents a user's authenticated session, associating it with a profile
57 // and some audit data.
68 type sortedSessions []Session
70 func (s sortedSessions) Len() int {
74 func (s sortedSessions) Less(i, j int) bool {
75 return s[i].Created.After(s[j].Created)
78 func (s sortedSessions) Swap(i, j int) {
79 s[i], s[j] = s[j], s[i]
82 type sessionStore interface {
83 createSession(session Session) error
84 getSession(id string) (Session, error)
85 removeSession(id string) error
86 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error)
89 func (m *memstore) createSession(session Session) error {
91 defer m.sessionLock.Unlock()
92 if _, ok := m.sessions[session.ID]; ok {
93 return ErrSessionAlreadyExists
95 m.sessions[session.ID] = session
99 func (m *memstore) getSession(id string) (Session, error) {
100 m.sessionLock.RLock()
101 defer m.sessionLock.RUnlock()
102 if _, ok := m.sessions[id]; !ok {
103 return Session{}, ErrSessionNotFound
105 return m.sessions[id], nil
108 func (m *memstore) removeSession(id string) error {
110 defer m.sessionLock.Unlock()
111 if _, ok := m.sessions[id]; !ok {
112 return ErrSessionNotFound
114 delete(m.sessions, id)
118 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
119 m.sessionLock.RLock()
120 defer m.sessionLock.RUnlock()
122 for _, session := range m.sessions {
123 if int64(len(res)) >= num {
126 if profile != nil && !profile.Equal(session.ProfileID) {
129 if !before.IsZero() && session.Created.After(before) {
132 res = append(res, session)
134 sorted := sortedSessions(res)
136 res = []Session(sorted)
140 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout.
141 func RegisterSessionHandlers(r *mux.Router, context Context) {
142 r.Handle("/login", wrap(context, CreateSessionHandler))
145 func checkCookie(r *http.Request, context Context) (Session, error) {
146 cookie, err := r.Cookie(authCookieName)
147 if err == http.ErrNoCookie {
148 return Session{}, ErrNoSession
149 } else if err != nil {
151 return Session{}, err
153 sess, err := context.GetSession(cookie.Value)
154 if err == ErrSessionNotFound {
155 return Session{}, ErrInvalidSession
156 } else if err != nil {
157 return Session{}, err
160 return Session{}, ErrInvalidSession
165 func buildLoginRedirect(r *http.Request, context Context) string {
166 if context.loginURI == nil {
169 uri := *context.loginURI
171 q.Set("from", r.URL.String())
172 uri.RawQuery = q.Encode()
176 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
177 realPass, err := hex.DecodeString(profile.Passphrase)
181 realSalt, err := hex.DecodeString(profile.Salt)
185 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
186 if !pass.Compare(candidate, realPass) {
187 return false, ErrIncorrectAuth
192 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
193 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
197 result = hex.EncodeToString(passBytes)
198 salt = hex.EncodeToString(saltBytes)
199 return result, salt, err
202 func pbkdf2sha256calc() (int, error) {
203 return pass.CalculateIterations(sha256.New)
206 func authenticate(user, passphrase string, context Context) (Profile, error) {
207 profile, err := context.GetProfileByLogin(user)
209 if err == ErrProfileNotFound || err == ErrLoginNotFound {
210 return Profile{}, ErrIncorrectAuth
212 return Profile{}, err
214 if profile.Compromised {
215 return Profile{}, ErrProfileCompromised
217 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
218 return profile, ErrProfileLocked
220 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
222 return Profile{}, ErrInvalidPassphraseScheme
224 result, err := scheme.check(profile, passphrase)
226 return Profile{}, err
231 // CreateSessionHandler allows the user to log into their account and create their session.
232 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) {
233 // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit
235 if r.Method == "POST" {
236 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
238 ip := r.Header.Get("X-Forwarded-For")
243 ID: uuid.NewID().String(),
245 UserAgent: r.UserAgent(),
246 ProfileID: profile.ID,
247 Login: r.PostFormValue("login"),
251 err = context.CreateSession(session)
253 w.WriteHeader(http.StatusInternalServerError)
254 w.Write([]byte(err.Error()))
257 // BUG(paddy): really need to do a security audit on our cookie
258 cookie := http.Cookie{
259 Name: authCookieName,
261 Expires: time.Now().Add(24 * 7 * time.Hour),
264 http.SetCookie(w, &cookie)
265 redirectTo := r.URL.Query().Get("from")
266 if redirectTo == "" {
269 http.Redirect(w, r, redirectTo, http.StatusFound)
271 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
272 w.WriteHeader(http.StatusInternalServerError)
273 w.Write([]byte(err.Error()))
276 errors = append(errors, err)
279 context.Render(w, loginTemplateName, map[string]interface{}{
284 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
285 enc := json.NewEncoder(w)
286 username := r.PostFormValue("username")
287 password := r.PostFormValue("password")
288 scope = r.PostFormValue("scope")
289 profile, err := authenticate(username, password, context)
291 if err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked {
292 w.WriteHeader(http.StatusBadRequest)
293 renderJSONError(enc, "invalid_grant")
296 w.WriteHeader(http.StatusInternalServerError)
297 w.Write([]byte(err.Error()))
300 profileID = profile.ID