auth
auth/session.go
Return client Secrets when listing clients with basic auth. If the request to list clients is sent with basic auth containing the login and password for the owner of the client, its secret is not removed from the response before sending it.
1 package auth
3 import (
4 "crypto/rand"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "log"
11 "net/http"
12 "sort"
13 "strings"
14 "time"
16 "code.secondbit.org/pass.hg"
17 "code.secondbit.org/uuid.hg"
18 "github.com/gorilla/mux"
19 )
21 const (
22 authCookieName = "auth"
23 loginTemplateName = "login"
24 )
26 func init() {
27 RegisterGrantType("password", GrantType{
28 Validate: credentialsValidate,
29 Invalidate: nil,
30 IssuesRefresh: true,
31 ReturnToken: RenderJSONToken,
32 AuditString: credentialsAuditString,
33 })
34 }
36 var (
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{
49 1: {
50 check: pbkdf2sha256check,
51 create: pbkdf2sha256create,
52 calculateIterations: pbkdf2sha256calc,
53 },
54 }
55 )
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)
61 }
63 // Session represents a user's authenticated session, associating it with a profile
64 // and some audit data.
65 type Session struct {
66 ID string
67 IP string
68 UserAgent string
69 ProfileID uuid.ID
70 Login string
71 Created time.Time
72 Expires time.Time
73 Active bool
74 CSRFToken string
75 }
77 type sortedSessions []Session
79 func (s sortedSessions) Len() int {
80 return len(s)
81 }
83 func (s sortedSessions) Less(i, j int) bool {
84 return s[i].Created.After(s[j].Created)
85 }
87 func (s sortedSessions) Swap(i, j int) {
88 s[i], s[j] = s[j], s[i]
89 }
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)
96 }
98 func (m *memstore) createSession(session Session) error {
99 m.sessionLock.Lock()
100 defer m.sessionLock.Unlock()
101 if _, ok := m.sessions[session.ID]; ok {
102 return ErrSessionAlreadyExists
103 }
104 m.sessions[session.ID] = session
105 return nil
106 }
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
113 }
114 return m.sessions[id], nil
115 }
117 func (m *memstore) removeSession(id string) error {
118 m.sessionLock.Lock()
119 defer m.sessionLock.Unlock()
120 if _, ok := m.sessions[id]; !ok {
121 return ErrSessionNotFound
122 }
123 delete(m.sessions, id)
124 return nil
125 }
127 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
128 m.sessionLock.RLock()
129 defer m.sessionLock.RUnlock()
130 res := []Session{}
131 for _, session := range m.sessions {
132 if int64(len(res)) >= num {
133 break
134 }
135 if profile != nil && !profile.Equal(session.ProfileID) {
136 continue
137 }
138 if !before.IsZero() && session.Created.After(before) {
139 continue
140 }
141 res = append(res, session)
142 }
143 sorted := sortedSessions(res)
144 sort.Sort(sorted)
145 res = []Session(sorted)
146 return res, nil
147 }
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.
154 }
156 func checkCSRF(r *http.Request, s Session) error {
157 if r.PostFormValue("csrftoken") != s.CSRFToken {
158 return ErrCSRFAttempt
159 }
160 return nil
161 }
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 {
168 log.Println(err)
169 return Session{}, err
170 }
171 sess, err := context.GetSession(cookie.Value)
172 if err == ErrSessionNotFound {
173 return Session{}, ErrInvalidSession
174 } else if err != nil {
175 return Session{}, err
176 }
177 if !sess.Active {
178 return Session{}, ErrInvalidSession
179 }
180 if time.Now().After(sess.Expires) {
181 return Session{}, ErrInvalidSession
182 }
183 return sess, nil
184 }
186 func buildLoginRedirect(r *http.Request, context Context) string {
187 if context.loginURI == nil {
188 return ""
189 }
190 uri := *context.loginURI
191 q := uri.Query()
192 q.Set("from", r.URL.String())
193 uri.RawQuery = q.Encode()
194 return uri.String()
195 }
197 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) {
198 realPass, err := hex.DecodeString(profile.Passphrase)
199 if err != nil {
200 return false, err
201 }
202 realSalt, err := hex.DecodeString(profile.Salt)
203 if err != nil {
204 return false, err
205 }
206 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt))
207 if !pass.Compare(candidate, realPass) {
208 return false, ErrIncorrectAuth
209 }
210 return true, nil
211 }
213 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) {
214 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase))
215 if err != nil {
216 return "", "", err
217 }
218 result = hex.EncodeToString(passBytes)
219 salt = hex.EncodeToString(saltBytes)
220 return result, salt, err
221 }
223 func pbkdf2sha256calc() (int, error) {
224 return pass.CalculateIterations(sha256.New)
225 }
227 func isAuthError(err error) bool {
228 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme
229 }
231 func authenticate(user, passphrase string, context Context) (Profile, error) {
232 profile, err := context.GetProfileByLogin(user)
233 if err != nil {
234 if err == ErrProfileNotFound || err == ErrLoginNotFound {
235 return Profile{}, ErrIncorrectAuth
236 }
237 return Profile{}, err
238 }
239 if profile.Compromised {
240 return Profile{}, ErrProfileCompromised
241 }
242 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) {
243 return profile, ErrProfileLocked
244 }
245 scheme, ok := passphraseSchemes[profile.PassphraseScheme]
246 if !ok {
247 return Profile{}, ErrInvalidPassphraseScheme
248 }
249 result, err := scheme.check(profile, passphrase)
250 if !result {
251 return Profile{}, err
252 }
253 return profile, nil
254 }
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) {
258 errors := []error{}
259 if r.Method == "POST" {
260 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context)
261 if err == nil {
262 ip := r.Header.Get("X-Forwarded-For")
263 if ip == "" {
264 ip = r.RemoteAddr
265 }
266 sessionID := make([]byte, 32)
267 csrfToken := make([]byte, 32)
268 _, err = rand.Read(sessionID)
269 if err != nil {
270 log.Println("Error reading CSPRNG for session ID:", err)
271 w.WriteHeader(http.StatusInternalServerError)
272 w.Write([]byte("Internal error"))
273 return
274 }
275 _, err = rand.Read(csrfToken)
276 if err != nil {
277 log.Println("Error reading CSPRNG for CSRF token:", err)
278 w.WriteHeader(http.StatusInternalServerError)
279 w.Write([]byte("internal error"))
280 return
281 }
282 session := Session{
283 ID: base64.StdEncoding.EncodeToString(sessionID),
284 IP: ip,
285 UserAgent: r.UserAgent(),
286 ProfileID: profile.ID,
287 Login: r.PostFormValue("login"),
288 Created: time.Now(),
289 Expires: time.Now().Add(time.Hour),
290 Active: true,
291 CSRFToken: base64.StdEncoding.EncodeToString(csrfToken),
292 }
293 err = context.CreateSession(session)
294 if err != nil {
295 w.WriteHeader(http.StatusInternalServerError)
296 w.Write([]byte(err.Error()))
297 return
298 }
299 // BUG(paddy): We really need to do a security audit on our cookies.
300 cookie := http.Cookie{
301 Name: authCookieName,
302 Value: session.ID,
303 Expires: session.Expires,
304 HttpOnly: true,
305 Secure: context.config.secureCookie,
306 }
307 http.SetCookie(w, &cookie)
308 redirectTo := r.URL.Query().Get("from")
309 if redirectTo == "" {
310 redirectTo = "/"
311 }
312 http.Redirect(w, r, redirectTo, http.StatusFound)
313 return
314 } else if !isAuthError(err) {
315 w.WriteHeader(http.StatusInternalServerError)
316 w.Write([]byte(err.Error()))
317 return
318 } else {
319 errors = append(errors, err)
320 }
321 }
322 context.Render(w, loginTemplateName, map[string]interface{}{
323 "errors": errors,
324 })
325 }
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)
333 if err != nil {
334 if isAuthError(err) {
335 w.WriteHeader(http.StatusBadRequest)
336 renderJSONError(enc, "invalid_grant")
337 return
338 }
339 w.WriteHeader(http.StatusInternalServerError)
340 w.Write([]byte(err.Error()))
341 return
342 }
343 profileID = profile.ID
344 valid = true
345 return
346 }
348 func credentialsAuditString(r *http.Request) string {
349 return "credentials"
350 }