auth
auth/session.go
Send events when logins are verified. Add an ActionLoginVerified constant to use as the action when a login has been verified. On second thought, this should probably just be "verified", huh? Then we can reuse it across models. Oops. We also added a call to send a login verified event to NSQ when the login is verified.
| 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 } |