auth
auth/session.go
Update client to detect errors. The client doesn't treat non-200 responses as errors automatically, so we need to detect when the response.Errors property is set, and use that to return an error. To avoid the boilerplate and an extensive error system, I just wrapped them in an httpErrors type that implements the error interface. That way the errors can be returned, and callers can type-cast and interrogate them. I also updated the GetLogin function to return an auth.ErrLoginNotFound error when the httpErrors response indicates that's the reason the request failed.
| paddy@70 | 1 package auth |
| paddy@70 | 2 |
| paddy@70 | 3 import ( |
| paddy@132 | 4 "crypto/rand" |
| paddy@98 | 5 "crypto/sha256" |
| paddy@132 | 6 "encoding/base64" |
| paddy@98 | 7 "encoding/hex" |
| paddy@119 | 8 "encoding/json" |
| paddy@70 | 9 "errors" |
| paddy@98 | 10 "log" |
| paddy@98 | 11 "net/http" |
| paddy@89 | 12 "sort" |
| paddy@135 | 13 "strings" |
| paddy@70 | 14 "time" |
| paddy@70 | 15 |
| paddy@107 | 16 "code.secondbit.org/pass.hg" |
| paddy@107 | 17 "code.secondbit.org/uuid.hg" |
| paddy@98 | 18 "github.com/gorilla/mux" |
| paddy@98 | 19 ) |
| paddy@98 | 20 |
| paddy@98 | 21 const ( |
| paddy@132 | 22 authCookieName = "auth" |
| paddy@98 | 23 loginTemplateName = "login" |
| paddy@70 | 24 ) |
| paddy@70 | 25 |
| paddy@119 | 26 func init() { |
| paddy@119 | 27 RegisterGrantType("password", GrantType{ |
| paddy@119 | 28 Validate: credentialsValidate, |
| paddy@119 | 29 Invalidate: nil, |
| paddy@119 | 30 IssuesRefresh: true, |
| paddy@119 | 31 ReturnToken: RenderJSONToken, |
| paddy@124 | 32 AuditString: credentialsAuditString, |
| paddy@119 | 33 }) |
| paddy@119 | 34 } |
| paddy@119 | 35 |
| paddy@70 | 36 var ( |
| paddy@70 | 37 // ErrNoSessionStore is returned when a Context tries to act on a sessionStore without setting on first. |
| paddy@70 | 38 ErrNoSessionStore = errors.New("no sessionStore was specified for the Context") |
| paddy@70 | 39 // ErrSessionNotFound is returned when a Session is requested but not found in the sessionStore. |
| paddy@70 | 40 ErrSessionNotFound = errors.New("session not found in sessionStore") |
| paddy@70 | 41 // ErrInvalidSession is returned when a Session is specified but is not valid. |
| paddy@70 | 42 ErrInvalidSession = errors.New("session is not valid") |
| paddy@77 | 43 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore. |
| paddy@77 | 44 ErrSessionAlreadyExists = errors.New("session already exists") |
| paddy@132 | 45 // ErrCSRFAttempt is returned when a CSRF attempt is detected. |
| paddy@132 | 46 ErrCSRFAttempt = errors.New("CSRF attempt") |
| paddy@98 | 47 |
| paddy@98 | 48 passphraseSchemes = map[int]passphraseScheme{ |
| paddy@98 | 49 1: { |
| paddy@98 | 50 check: pbkdf2sha256check, |
| paddy@98 | 51 create: pbkdf2sha256create, |
| paddy@98 | 52 calculateIterations: pbkdf2sha256calc, |
| paddy@98 | 53 }, |
| paddy@98 | 54 } |
| paddy@70 | 55 ) |
| paddy@70 | 56 |
| paddy@98 | 57 type passphraseScheme struct { |
| paddy@98 | 58 check func(profile Profile, passphrase string) (bool, error) |
| paddy@103 | 59 create func(passphrase string, iterations int) (result, salt string, err error) |
| paddy@98 | 60 calculateIterations func() (int, error) |
| paddy@98 | 61 } |
| paddy@98 | 62 |
| paddy@70 | 63 // Session represents a user's authenticated session, associating it with a profile |
| paddy@70 | 64 // and some audit data. |
| paddy@70 | 65 type Session struct { |
| paddy@70 | 66 ID string |
| paddy@70 | 67 IP string |
| paddy@70 | 68 UserAgent string |
| paddy@70 | 69 ProfileID uuid.ID |
| paddy@98 | 70 Login string |
| paddy@70 | 71 Created time.Time |
| paddy@132 | 72 Expires time.Time |
| paddy@70 | 73 Active bool |
| paddy@132 | 74 CSRFToken string |
| paddy@70 | 75 } |
| paddy@70 | 76 |
| paddy@89 | 77 type sortedSessions []Session |
| paddy@89 | 78 |
| paddy@89 | 79 func (s sortedSessions) Len() int { |
| paddy@89 | 80 return len(s) |
| paddy@89 | 81 } |
| paddy@89 | 82 |
| paddy@89 | 83 func (s sortedSessions) Less(i, j int) bool { |
| paddy@89 | 84 return s[i].Created.After(s[j].Created) |
| paddy@89 | 85 } |
| paddy@89 | 86 |
| paddy@89 | 87 func (s sortedSessions) Swap(i, j int) { |
| paddy@89 | 88 s[i], s[j] = s[j], s[i] |
| paddy@89 | 89 } |
| paddy@89 | 90 |
| paddy@70 | 91 type sessionStore interface { |
| paddy@70 | 92 createSession(session Session) error |
| paddy@70 | 93 getSession(id string) (Session, error) |
| paddy@159 | 94 terminateSession(id string) error |
| paddy@70 | 95 removeSession(id string) error |
| paddy@70 | 96 listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) |
| paddy@162 | 97 terminateSessionsByProfile(profile uuid.ID) error |
| paddy@70 | 98 } |
| paddy@77 | 99 |
| paddy@77 | 100 func (m *memstore) createSession(session Session) error { |
| paddy@77 | 101 m.sessionLock.Lock() |
| paddy@77 | 102 defer m.sessionLock.Unlock() |
| paddy@77 | 103 if _, ok := m.sessions[session.ID]; ok { |
| paddy@77 | 104 return ErrSessionAlreadyExists |
| paddy@77 | 105 } |
| paddy@77 | 106 m.sessions[session.ID] = session |
| paddy@77 | 107 return nil |
| paddy@77 | 108 } |
| paddy@77 | 109 |
| paddy@77 | 110 func (m *memstore) getSession(id string) (Session, error) { |
| paddy@77 | 111 m.sessionLock.RLock() |
| paddy@77 | 112 defer m.sessionLock.RUnlock() |
| paddy@77 | 113 if _, ok := m.sessions[id]; !ok { |
| paddy@77 | 114 return Session{}, ErrSessionNotFound |
| paddy@77 | 115 } |
| paddy@77 | 116 return m.sessions[id], nil |
| paddy@77 | 117 } |
| paddy@77 | 118 |
| paddy@159 | 119 func (m *memstore) terminateSession(id string) error { |
| paddy@159 | 120 m.sessionLock.RLock() |
| paddy@159 | 121 defer m.sessionLock.RUnlock() |
| paddy@159 | 122 sess, ok := m.sessions[id] |
| paddy@159 | 123 if !ok { |
| paddy@159 | 124 return ErrSessionNotFound |
| paddy@159 | 125 } |
| paddy@159 | 126 sess.Active = false |
| paddy@159 | 127 m.sessions[id] = sess |
| paddy@159 | 128 return nil |
| paddy@159 | 129 } |
| paddy@159 | 130 |
| paddy@162 | 131 func (m *memstore) terminateSessionsByProfile(profile uuid.ID) error { |
| paddy@162 | 132 m.sessionLock.RLock() |
| paddy@162 | 133 defer m.sessionLock.RUnlock() |
| paddy@162 | 134 var found bool |
| paddy@162 | 135 for _, session := range m.sessions { |
| paddy@162 | 136 if profile.Equal(session.ProfileID) { |
| paddy@162 | 137 session.Active = false |
| paddy@162 | 138 m.sessions[session.ID] = session |
| paddy@162 | 139 found = true |
| paddy@162 | 140 } |
| paddy@162 | 141 } |
| paddy@162 | 142 if !found { |
| paddy@162 | 143 return ErrProfileNotFound |
| paddy@162 | 144 } |
| paddy@162 | 145 return nil |
| paddy@162 | 146 } |
| paddy@162 | 147 |
| paddy@77 | 148 func (m *memstore) removeSession(id string) error { |
| paddy@77 | 149 m.sessionLock.Lock() |
| paddy@77 | 150 defer m.sessionLock.Unlock() |
| paddy@77 | 151 if _, ok := m.sessions[id]; !ok { |
| paddy@77 | 152 return ErrSessionNotFound |
| paddy@77 | 153 } |
| paddy@77 | 154 delete(m.sessions, id) |
| paddy@77 | 155 return nil |
| paddy@77 | 156 } |
| paddy@77 | 157 |
| paddy@77 | 158 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) { |
| paddy@77 | 159 m.sessionLock.RLock() |
| paddy@77 | 160 defer m.sessionLock.RUnlock() |
| paddy@77 | 161 res := []Session{} |
| paddy@77 | 162 for _, session := range m.sessions { |
| paddy@77 | 163 if int64(len(res)) >= num { |
| paddy@77 | 164 break |
| paddy@77 | 165 } |
| paddy@77 | 166 if profile != nil && !profile.Equal(session.ProfileID) { |
| paddy@77 | 167 continue |
| paddy@77 | 168 } |
| paddy@77 | 169 if !before.IsZero() && session.Created.After(before) { |
| paddy@77 | 170 continue |
| paddy@77 | 171 } |
| paddy@77 | 172 res = append(res, session) |
| paddy@77 | 173 } |
| paddy@89 | 174 sorted := sortedSessions(res) |
| paddy@89 | 175 sort.Sort(sorted) |
| paddy@89 | 176 res = []Session(sorted) |
| paddy@77 | 177 return res, nil |
| paddy@77 | 178 } |
| paddy@98 | 179 |
| paddy@98 | 180 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout. |
| paddy@98 | 181 func RegisterSessionHandlers(r *mux.Router, context Context) { |
| paddy@98 | 182 r.Handle("/login", wrap(context, CreateSessionHandler)) |
| paddy@128 | 183 // BUG(paddy): We need to implement a handler for listing sessions active on a profile. |
| paddy@159 | 184 r.Handle("/sessions/{id}", wrap(context, TerminateSessionHandler)).Methods("OPTIONS", "DELETE") |
| paddy@98 | 185 } |
| paddy@98 | 186 |
| paddy@132 | 187 func checkCSRF(r *http.Request, s Session) error { |
| paddy@132 | 188 if r.PostFormValue("csrftoken") != s.CSRFToken { |
| paddy@132 | 189 return ErrCSRFAttempt |
| paddy@132 | 190 } |
| paddy@132 | 191 return nil |
| paddy@132 | 192 } |
| paddy@132 | 193 |
| paddy@98 | 194 func checkCookie(r *http.Request, context Context) (Session, error) { |
| paddy@98 | 195 cookie, err := r.Cookie(authCookieName) |
| paddy@98 | 196 if err == http.ErrNoCookie { |
| paddy@98 | 197 return Session{}, ErrNoSession |
| paddy@98 | 198 } else if err != nil { |
| paddy@98 | 199 log.Println(err) |
| paddy@98 | 200 return Session{}, err |
| paddy@98 | 201 } |
| paddy@98 | 202 sess, err := context.GetSession(cookie.Value) |
| paddy@98 | 203 if err == ErrSessionNotFound { |
| paddy@98 | 204 return Session{}, ErrInvalidSession |
| paddy@98 | 205 } else if err != nil { |
| paddy@98 | 206 return Session{}, err |
| paddy@98 | 207 } |
| paddy@98 | 208 if !sess.Active { |
| paddy@98 | 209 return Session{}, ErrInvalidSession |
| paddy@98 | 210 } |
| paddy@132 | 211 if time.Now().After(sess.Expires) { |
| paddy@132 | 212 return Session{}, ErrInvalidSession |
| paddy@132 | 213 } |
| paddy@98 | 214 return sess, nil |
| paddy@98 | 215 } |
| paddy@98 | 216 |
| paddy@98 | 217 func buildLoginRedirect(r *http.Request, context Context) string { |
| paddy@98 | 218 if context.loginURI == nil { |
| paddy@98 | 219 return "" |
| paddy@98 | 220 } |
| paddy@98 | 221 uri := *context.loginURI |
| paddy@98 | 222 q := uri.Query() |
| paddy@98 | 223 q.Set("from", r.URL.String()) |
| paddy@98 | 224 uri.RawQuery = q.Encode() |
| paddy@98 | 225 return uri.String() |
| paddy@98 | 226 } |
| paddy@98 | 227 |
| paddy@98 | 228 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) { |
| paddy@98 | 229 realPass, err := hex.DecodeString(profile.Passphrase) |
| paddy@98 | 230 if err != nil { |
| paddy@98 | 231 return false, err |
| paddy@98 | 232 } |
| paddy@103 | 233 realSalt, err := hex.DecodeString(profile.Salt) |
| paddy@103 | 234 if err != nil { |
| paddy@103 | 235 return false, err |
| paddy@103 | 236 } |
| paddy@103 | 237 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt)) |
| paddy@98 | 238 if !pass.Compare(candidate, realPass) { |
| paddy@98 | 239 return false, ErrIncorrectAuth |
| paddy@98 | 240 } |
| paddy@98 | 241 return true, nil |
| paddy@98 | 242 } |
| paddy@98 | 243 |
| paddy@103 | 244 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) { |
| paddy@103 | 245 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase)) |
| paddy@103 | 246 if err != nil { |
| paddy@103 | 247 return "", "", err |
| paddy@103 | 248 } |
| paddy@103 | 249 result = hex.EncodeToString(passBytes) |
| paddy@103 | 250 salt = hex.EncodeToString(saltBytes) |
| paddy@103 | 251 return result, salt, err |
| paddy@98 | 252 } |
| paddy@98 | 253 |
| paddy@98 | 254 func pbkdf2sha256calc() (int, error) { |
| paddy@98 | 255 return pass.CalculateIterations(sha256.New) |
| paddy@98 | 256 } |
| paddy@98 | 257 |
| paddy@139 | 258 func isAuthError(err error) bool { |
| paddy@139 | 259 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme |
| paddy@139 | 260 } |
| paddy@139 | 261 |
| paddy@98 | 262 func authenticate(user, passphrase string, context Context) (Profile, error) { |
| paddy@98 | 263 profile, err := context.GetProfileByLogin(user) |
| paddy@98 | 264 if err != nil { |
| paddy@98 | 265 if err == ErrProfileNotFound || err == ErrLoginNotFound { |
| paddy@98 | 266 return Profile{}, ErrIncorrectAuth |
| paddy@98 | 267 } |
| paddy@98 | 268 return Profile{}, err |
| paddy@98 | 269 } |
| paddy@98 | 270 if profile.Compromised { |
| paddy@98 | 271 return Profile{}, ErrProfileCompromised |
| paddy@98 | 272 } |
| paddy@98 | 273 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) { |
| paddy@98 | 274 return profile, ErrProfileLocked |
| paddy@98 | 275 } |
| paddy@98 | 276 scheme, ok := passphraseSchemes[profile.PassphraseScheme] |
| paddy@98 | 277 if !ok { |
| paddy@98 | 278 return Profile{}, ErrInvalidPassphraseScheme |
| paddy@98 | 279 } |
| paddy@98 | 280 result, err := scheme.check(profile, passphrase) |
| paddy@98 | 281 if !result { |
| paddy@98 | 282 return Profile{}, err |
| paddy@98 | 283 } |
| paddy@98 | 284 return profile, nil |
| paddy@98 | 285 } |
| paddy@98 | 286 |
| paddy@98 | 287 // CreateSessionHandler allows the user to log into their account and create their session. |
| paddy@98 | 288 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@98 | 289 errors := []error{} |
| paddy@98 | 290 if r.Method == "POST" { |
| paddy@98 | 291 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context) |
| paddy@98 | 292 if err == nil { |
| paddy@98 | 293 ip := r.Header.Get("X-Forwarded-For") |
| paddy@98 | 294 if ip == "" { |
| paddy@98 | 295 ip = r.RemoteAddr |
| paddy@98 | 296 } |
| paddy@132 | 297 sessionID := make([]byte, 32) |
| paddy@132 | 298 csrfToken := make([]byte, 32) |
| paddy@132 | 299 _, err = rand.Read(sessionID) |
| paddy@132 | 300 if err != nil { |
| paddy@132 | 301 log.Println("Error reading CSPRNG for session ID:", err) |
| paddy@132 | 302 w.WriteHeader(http.StatusInternalServerError) |
| paddy@132 | 303 w.Write([]byte("Internal error")) |
| paddy@132 | 304 return |
| paddy@132 | 305 } |
| paddy@132 | 306 _, err = rand.Read(csrfToken) |
| paddy@132 | 307 if err != nil { |
| paddy@132 | 308 log.Println("Error reading CSPRNG for CSRF token:", err) |
| paddy@132 | 309 w.WriteHeader(http.StatusInternalServerError) |
| paddy@132 | 310 w.Write([]byte("internal error")) |
| paddy@132 | 311 return |
| paddy@132 | 312 } |
| paddy@98 | 313 session := Session{ |
| paddy@159 | 314 ID: base64.URLEncoding.EncodeToString(sessionID), |
| paddy@98 | 315 IP: ip, |
| paddy@98 | 316 UserAgent: r.UserAgent(), |
| paddy@98 | 317 ProfileID: profile.ID, |
| paddy@98 | 318 Login: r.PostFormValue("login"), |
| paddy@98 | 319 Created: time.Now(), |
| paddy@132 | 320 Expires: time.Now().Add(time.Hour), |
| paddy@98 | 321 Active: true, |
| paddy@159 | 322 CSRFToken: base64.URLEncoding.EncodeToString(csrfToken), |
| paddy@98 | 323 } |
| paddy@98 | 324 err = context.CreateSession(session) |
| paddy@98 | 325 if err != nil { |
| paddy@98 | 326 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 327 w.Write([]byte(err.Error())) |
| paddy@98 | 328 return |
| paddy@98 | 329 } |
| paddy@132 | 330 // BUG(paddy): We really need to do a security audit on our cookies. |
| paddy@98 | 331 cookie := http.Cookie{ |
| paddy@98 | 332 Name: authCookieName, |
| paddy@98 | 333 Value: session.ID, |
| paddy@132 | 334 Expires: session.Expires, |
| paddy@98 | 335 HttpOnly: true, |
| paddy@132 | 336 Secure: context.config.secureCookie, |
| paddy@98 | 337 } |
| paddy@98 | 338 http.SetCookie(w, &cookie) |
| paddy@98 | 339 redirectTo := r.URL.Query().Get("from") |
| paddy@98 | 340 if redirectTo == "" { |
| paddy@98 | 341 redirectTo = "/" |
| paddy@98 | 342 } |
| paddy@98 | 343 http.Redirect(w, r, redirectTo, http.StatusFound) |
| paddy@98 | 344 return |
| paddy@139 | 345 } else if !isAuthError(err) { |
| paddy@98 | 346 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 347 w.Write([]byte(err.Error())) |
| paddy@98 | 348 return |
| paddy@98 | 349 } else { |
| paddy@98 | 350 errors = append(errors, err) |
| paddy@98 | 351 } |
| paddy@98 | 352 } |
| paddy@98 | 353 context.Render(w, loginTemplateName, map[string]interface{}{ |
| paddy@98 | 354 "errors": errors, |
| paddy@98 | 355 }) |
| paddy@98 | 356 } |
| paddy@119 | 357 |
| paddy@159 | 358 // TerminateSessionHandler allows the user to end their session before it expires. |
| paddy@159 | 359 func TerminateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 360 var errors []RequestError |
| paddy@159 | 361 vars := mux.Vars(r) |
| paddy@159 | 362 if vars["id"] == "" { |
| paddy@172 | 363 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 364 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@159 | 365 return |
| paddy@159 | 366 } |
| paddy@159 | 367 id := vars["id"] |
| paddy@159 | 368 un, pw, ok := r.BasicAuth() |
| paddy@159 | 369 if !ok { |
| paddy@172 | 370 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 371 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@159 | 372 return |
| paddy@159 | 373 } |
| paddy@159 | 374 profile, err := authenticate(un, pw, context) |
| paddy@159 | 375 if err != nil { |
| paddy@159 | 376 if isAuthError(err) { |
| paddy@172 | 377 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 378 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@159 | 379 return |
| paddy@159 | 380 } |
| paddy@172 | 381 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 382 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@159 | 383 return |
| paddy@159 | 384 } |
| paddy@159 | 385 session, err := context.GetSession(id) |
| paddy@159 | 386 if err != nil { |
| paddy@159 | 387 if err == ErrSessionNotFound { |
| paddy@172 | 388 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 389 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@159 | 390 return |
| paddy@159 | 391 } |
| paddy@172 | 392 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 393 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@159 | 394 return |
| paddy@159 | 395 } |
| paddy@159 | 396 if !session.ProfileID.Equal(profile.ID) { |
| paddy@172 | 397 errors = append(errors, RequestError{Slug: RequestErrAccessDenied, Param: "id"}) |
| paddy@172 | 398 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@159 | 399 return |
| paddy@159 | 400 } |
| paddy@159 | 401 err = context.TerminateSession(id) |
| paddy@159 | 402 if err != nil { |
| paddy@159 | 403 if err == ErrSessionNotFound { |
| paddy@172 | 404 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 405 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@159 | 406 return |
| paddy@159 | 407 } |
| paddy@172 | 408 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 409 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@159 | 410 return |
| paddy@159 | 411 } |
| paddy@159 | 412 session.Active = false |
| paddy@172 | 413 encode(w, r, http.StatusOK, Response{Sessions: []Session{session}, Errors: errors}) |
| paddy@159 | 414 } |
| paddy@159 | 415 |
| paddy@163 | 416 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) { |
| paddy@119 | 417 enc := json.NewEncoder(w) |
| paddy@119 | 418 username := r.PostFormValue("username") |
| paddy@119 | 419 password := r.PostFormValue("password") |
| paddy@163 | 420 scopes = stringsToScopes(strings.Split(r.PostFormValue("scope"), " ")) |
| paddy@119 | 421 profile, err := authenticate(username, password, context) |
| paddy@119 | 422 if err != nil { |
| paddy@139 | 423 if isAuthError(err) { |
| paddy@119 | 424 w.WriteHeader(http.StatusBadRequest) |
| paddy@119 | 425 renderJSONError(enc, "invalid_grant") |
| paddy@119 | 426 return |
| paddy@119 | 427 } |
| paddy@119 | 428 w.WriteHeader(http.StatusInternalServerError) |
| paddy@119 | 429 w.Write([]byte(err.Error())) |
| paddy@119 | 430 return |
| paddy@119 | 431 } |
| paddy@119 | 432 profileID = profile.ID |
| paddy@119 | 433 valid = true |
| paddy@119 | 434 return |
| paddy@119 | 435 } |
| paddy@124 | 436 |
| paddy@124 | 437 func credentialsAuditString(r *http.Request) string { |
| paddy@124 | 438 return "credentials" |
| paddy@124 | 439 } |