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