auth

Paddy 2015-01-18 Parent:118a69954621 Child:d14f0a81498c

123:0a1e16b9c141 Go to Latest

auth/session.go

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.

History
1 package auth
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "log"
9 "net/http"
10 "sort"
11 "time"
13 "code.secondbit.org/pass.hg"
14 "code.secondbit.org/uuid.hg"
15 "github.com/gorilla/mux"
16 )
18 const (
19 loginTemplateName = "login"
20 )
22 func init() {
23 RegisterGrantType("password", GrantType{
24 Validate: credentialsValidate,
25 Invalidate: nil,
26 IssuesRefresh: true,
27 ReturnToken: RenderJSONToken,
28 })
29 }
31 var (
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{
42 1: {
43 check: pbkdf2sha256check,
44 create: pbkdf2sha256create,
45 calculateIterations: pbkdf2sha256calc,
46 },
47 }
48 )
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)
54 }
56 // Session represents a user's authenticated session, associating it with a profile
57 // and some audit data.
58 type Session struct {
59 ID string
60 IP string
61 UserAgent string
62 ProfileID uuid.ID
63 Login string
64 Created time.Time
65 Active bool
66 }
68 type sortedSessions []Session
70 func (s sortedSessions) Len() int {
71 return len(s)
72 }
74 func (s sortedSessions) Less(i, j int) bool {
75 return s[i].Created.After(s[j].Created)
76 }
78 func (s sortedSessions) Swap(i, j int) {
79 s[i], s[j] = s[j], s[i]
80 }
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)
87 }
89 func (m *memstore) createSession(session Session) error {
90 m.sessionLock.Lock()
91 defer m.sessionLock.Unlock()
92 if _, ok := m.sessions[session.ID]; ok {
93 return ErrSessionAlreadyExists
94 }
95 m.sessions[session.ID] = session
96 return nil
97 }
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
104 }
105 return m.sessions[id], nil
106 }
108 func (m *memstore) removeSession(id string) error {
109 m.sessionLock.Lock()
110 defer m.sessionLock.Unlock()
111 if _, ok := m.sessions[id]; !ok {
112 return ErrSessionNotFound
113 }
114 delete(m.sessions, id)
115 return nil
116 }
118 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
119 m.sessionLock.RLock()
120 defer m.sessionLock.RUnlock()
121 res := []Session{}
122 for _, session := range m.sessions {
123 if int64(len(res)) >= num {
124 break
125 }
126 if profile != nil && !profile.Equal(session.ProfileID) {
127 continue
128 }
129 if !before.IsZero() && session.Created.After(before) {
130 continue
131 }
132 res = append(res, session)
133 }
134 sorted := sortedSessions(res)
135 sort.Sort(sorted)
136 res = []Session(sorted)
137 return res, nil
138 }
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))
143 }
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 {
150 log.Println(err)
151 return Session{}, err
152 }
153 sess, err := context.GetSession(cookie.Value)
154 if err == ErrSessionNotFound {
155 return Session{}, ErrInvalidSession
156 } else if err != nil {
157 return Session{}, err
158 }
159 if !sess.Active {
160 return Session{}, ErrInvalidSession
161 }
162 return sess, nil
163 }
165 func buildLoginRedirect(r *http.Request, context Context) string {
166 if context.loginURI == nil {
167 return ""
168 }
169 uri := *context.loginURI
170 q := uri.Query()
171 q.Set("from", r.URL.String())
172 uri.RawQuery = q.Encode()
173 return uri.String()
174 }
176 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
177 realPass, err := hex.DecodeString(profile.Passphrase)
178 if err != nil {
179 return false, err
180 }
181 realSalt, err := hex.DecodeString(profile.Salt)
182 if err != nil {
183 return false, err
184 }
185 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
186 if !pass.Compare(candidate, realPass) {
187 return false, ErrIncorrectAuth
188 }
189 return true, nil
190 }
192 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
193 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
194 if err != nil {
195 return "", "", err
196 }
197 result = hex.EncodeToString(passBytes)
198 salt = hex.EncodeToString(saltBytes)
199 return result, salt, err
200 }
202 func pbkdf2sha256calc() (int, error) {
203 return pass.CalculateIterations(sha256.New)
204 }
206 func authenticate(user, passphrase string, context Context) (Profile, error) {
207 profile, err := context.GetProfileByLogin(user)
208 if err != nil {
209 if err == ErrProfileNotFound || err == ErrLoginNotFound {
210 return Profile{}, ErrIncorrectAuth
211 }
212 return Profile{}, err
213 }
214 if profile.Compromised {
215 return Profile{}, ErrProfileCompromised
216 }
217 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
218 return profile, ErrProfileLocked
219 }
220 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
221 if !ok {
222 return Profile{}, ErrInvalidPassphraseScheme
223 }
224 result, err := scheme.check(profile, passphrase)
225 if !result {
226 return Profile{}, err
227 }
228 return profile, nil
229 }
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
234 errors := []error{}
235 if r.Method == "POST" {
236 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
237 if err == nil {
238 ip := r.Header.Get("X-Forwarded-For")
239 if ip == "" {
240 ip = r.RemoteAddr
241 }
242 session := Session{
243 ID: uuid.NewID().String(),
244 IP: ip,
245 UserAgent: r.UserAgent(),
246 ProfileID: profile.ID,
247 Login: r.PostFormValue("login"),
248 Created: time.Now(),
249 Active: true,
250 }
251 err = context.CreateSession(session)
252 if err != nil {
253 w.WriteHeader(http.StatusInternalServerError)
254 w.Write([]byte(err.Error()))
255 return
256 }
257 // BUG(paddy): really need to do a security audit on our cookie
258 cookie := http.Cookie{
259 Name: authCookieName,
260 Value: session.ID,
261 Expires: time.Now().Add(24 * 7 * time.Hour),
262 HttpOnly: true,
263 }
264 http.SetCookie(w, &cookie)
265 redirectTo := r.URL.Query().Get("from")
266 if redirectTo == "" {
267 redirectTo = "/"
268 }
269 http.Redirect(w, r, redirectTo, http.StatusFound)
270 return
271 } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked {
272 w.WriteHeader(http.StatusInternalServerError)
273 w.Write([]byte(err.Error()))
274 return
275 } else {
276 errors = append(errors, err)
277 }
278 }
279 context.Render(w, loginTemplateName, map[string]interface{}{
280 "errors": errors,
281 })
282 }
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)
290 if err != nil {
291 if err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked {
292 w.WriteHeader(http.StatusBadRequest)
293 renderJSONError(enc, "invalid_grant")
294 return
295 }
296 w.WriteHeader(http.StatusInternalServerError)
297 w.Write([]byte(err.Error()))
298 return
299 }
300 profileID = profile.ID
301 valid = true
302 return
303 }