auth
auth/session.go
Stop soft-deleting Profiles and actually delete them. The information we're storing in Profiles isn't unique enough that we should go through the hassle we're going through to soft-delete it. Add a deleteProfile method to our profileStore, and implement it for our postgres and memstore implementations. Add a DeleteProfile wrapper for our Context. Remove the Deleted property from the Profile type and the ProfileChange type, and update references to it. Stop cleaning up after our Profile in the UpdateProfileHandler, because there's no longer any way to delete the Profile from the UpdateProfileHandler. Update our get/list* methods so they don't filter on the non-existent Deleted property anymore. Update our SQL schema definition to not include the deleted column. Update our profile tests to use the DeleteProfile method and stop comparing the no-longer-existing Deleted property.
| 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@70 | 97 } |
| paddy@77 | 98 |
| paddy@77 | 99 func (m *memstore) createSession(session Session) error { |
| paddy@77 | 100 m.sessionLock.Lock() |
| paddy@77 | 101 defer m.sessionLock.Unlock() |
| paddy@77 | 102 if _, ok := m.sessions[session.ID]; ok { |
| paddy@77 | 103 return ErrSessionAlreadyExists |
| paddy@77 | 104 } |
| paddy@77 | 105 m.sessions[session.ID] = session |
| paddy@77 | 106 return nil |
| paddy@77 | 107 } |
| paddy@77 | 108 |
| paddy@77 | 109 func (m *memstore) getSession(id string) (Session, error) { |
| paddy@77 | 110 m.sessionLock.RLock() |
| paddy@77 | 111 defer m.sessionLock.RUnlock() |
| paddy@77 | 112 if _, ok := m.sessions[id]; !ok { |
| paddy@77 | 113 return Session{}, ErrSessionNotFound |
| paddy@77 | 114 } |
| paddy@77 | 115 return m.sessions[id], nil |
| paddy@77 | 116 } |
| paddy@77 | 117 |
| paddy@159 | 118 func (m *memstore) terminateSession(id string) error { |
| paddy@159 | 119 m.sessionLock.RLock() |
| paddy@159 | 120 defer m.sessionLock.RUnlock() |
| paddy@159 | 121 sess, ok := m.sessions[id] |
| paddy@159 | 122 if !ok { |
| paddy@159 | 123 return ErrSessionNotFound |
| paddy@159 | 124 } |
| paddy@159 | 125 sess.Active = false |
| paddy@159 | 126 m.sessions[id] = sess |
| paddy@159 | 127 return nil |
| paddy@159 | 128 } |
| paddy@159 | 129 |
| paddy@77 | 130 func (m *memstore) removeSession(id string) error { |
| paddy@77 | 131 m.sessionLock.Lock() |
| paddy@77 | 132 defer m.sessionLock.Unlock() |
| paddy@77 | 133 if _, ok := m.sessions[id]; !ok { |
| paddy@77 | 134 return ErrSessionNotFound |
| paddy@77 | 135 } |
| paddy@77 | 136 delete(m.sessions, id) |
| paddy@77 | 137 return nil |
| paddy@77 | 138 } |
| paddy@77 | 139 |
| paddy@77 | 140 func (m *memstore) listSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) { |
| paddy@77 | 141 m.sessionLock.RLock() |
| paddy@77 | 142 defer m.sessionLock.RUnlock() |
| paddy@77 | 143 res := []Session{} |
| paddy@77 | 144 for _, session := range m.sessions { |
| paddy@77 | 145 if int64(len(res)) >= num { |
| paddy@77 | 146 break |
| paddy@77 | 147 } |
| paddy@77 | 148 if profile != nil && !profile.Equal(session.ProfileID) { |
| paddy@77 | 149 continue |
| paddy@77 | 150 } |
| paddy@77 | 151 if !before.IsZero() && session.Created.After(before) { |
| paddy@77 | 152 continue |
| paddy@77 | 153 } |
| paddy@77 | 154 res = append(res, session) |
| paddy@77 | 155 } |
| paddy@89 | 156 sorted := sortedSessions(res) |
| paddy@89 | 157 sort.Sort(sorted) |
| paddy@89 | 158 res = []Session(sorted) |
| paddy@77 | 159 return res, nil |
| paddy@77 | 160 } |
| paddy@98 | 161 |
| paddy@98 | 162 // RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout. |
| paddy@98 | 163 func RegisterSessionHandlers(r *mux.Router, context Context) { |
| paddy@98 | 164 r.Handle("/login", wrap(context, CreateSessionHandler)) |
| paddy@128 | 165 // BUG(paddy): We need to implement a handler for listing sessions active on a profile. |
| paddy@159 | 166 r.Handle("/sessions/{id}", wrap(context, TerminateSessionHandler)).Methods("OPTIONS", "DELETE") |
| paddy@98 | 167 } |
| paddy@98 | 168 |
| paddy@132 | 169 func checkCSRF(r *http.Request, s Session) error { |
| paddy@132 | 170 if r.PostFormValue("csrftoken") != s.CSRFToken { |
| paddy@132 | 171 return ErrCSRFAttempt |
| paddy@132 | 172 } |
| paddy@132 | 173 return nil |
| paddy@132 | 174 } |
| paddy@132 | 175 |
| paddy@98 | 176 func checkCookie(r *http.Request, context Context) (Session, error) { |
| paddy@98 | 177 cookie, err := r.Cookie(authCookieName) |
| paddy@98 | 178 if err == http.ErrNoCookie { |
| paddy@98 | 179 return Session{}, ErrNoSession |
| paddy@98 | 180 } else if err != nil { |
| paddy@98 | 181 log.Println(err) |
| paddy@98 | 182 return Session{}, err |
| paddy@98 | 183 } |
| paddy@98 | 184 sess, err := context.GetSession(cookie.Value) |
| paddy@98 | 185 if err == ErrSessionNotFound { |
| paddy@98 | 186 return Session{}, ErrInvalidSession |
| paddy@98 | 187 } else if err != nil { |
| paddy@98 | 188 return Session{}, err |
| paddy@98 | 189 } |
| paddy@98 | 190 if !sess.Active { |
| paddy@98 | 191 return Session{}, ErrInvalidSession |
| paddy@98 | 192 } |
| paddy@132 | 193 if time.Now().After(sess.Expires) { |
| paddy@132 | 194 return Session{}, ErrInvalidSession |
| paddy@132 | 195 } |
| paddy@98 | 196 return sess, nil |
| paddy@98 | 197 } |
| paddy@98 | 198 |
| paddy@98 | 199 func buildLoginRedirect(r *http.Request, context Context) string { |
| paddy@98 | 200 if context.loginURI == nil { |
| paddy@98 | 201 return "" |
| paddy@98 | 202 } |
| paddy@98 | 203 uri := *context.loginURI |
| paddy@98 | 204 q := uri.Query() |
| paddy@98 | 205 q.Set("from", r.URL.String()) |
| paddy@98 | 206 uri.RawQuery = q.Encode() |
| paddy@98 | 207 return uri.String() |
| paddy@98 | 208 } |
| paddy@98 | 209 |
| paddy@98 | 210 func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) { |
| paddy@98 | 211 realPass, err := hex.DecodeString(profile.Passphrase) |
| paddy@98 | 212 if err != nil { |
| paddy@98 | 213 return false, err |
| paddy@98 | 214 } |
| paddy@103 | 215 realSalt, err := hex.DecodeString(profile.Salt) |
| paddy@103 | 216 if err != nil { |
| paddy@103 | 217 return false, err |
| paddy@103 | 218 } |
| paddy@103 | 219 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(realSalt)) |
| paddy@98 | 220 if !pass.Compare(candidate, realPass) { |
| paddy@98 | 221 return false, ErrIncorrectAuth |
| paddy@98 | 222 } |
| paddy@98 | 223 return true, nil |
| paddy@98 | 224 } |
| paddy@98 | 225 |
| paddy@103 | 226 func pbkdf2sha256create(passphrase string, iters int) (result, salt string, err error) { |
| paddy@103 | 227 passBytes, saltBytes, err := pass.Create(sha256.New, iters, []byte(passphrase)) |
| paddy@103 | 228 if err != nil { |
| paddy@103 | 229 return "", "", err |
| paddy@103 | 230 } |
| paddy@103 | 231 result = hex.EncodeToString(passBytes) |
| paddy@103 | 232 salt = hex.EncodeToString(saltBytes) |
| paddy@103 | 233 return result, salt, err |
| paddy@98 | 234 } |
| paddy@98 | 235 |
| paddy@98 | 236 func pbkdf2sha256calc() (int, error) { |
| paddy@98 | 237 return pass.CalculateIterations(sha256.New) |
| paddy@98 | 238 } |
| paddy@98 | 239 |
| paddy@139 | 240 func isAuthError(err error) bool { |
| paddy@139 | 241 return err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked || err == ErrInvalidPassphraseScheme |
| paddy@139 | 242 } |
| paddy@139 | 243 |
| paddy@98 | 244 func authenticate(user, passphrase string, context Context) (Profile, error) { |
| paddy@98 | 245 profile, err := context.GetProfileByLogin(user) |
| paddy@98 | 246 if err != nil { |
| paddy@98 | 247 if err == ErrProfileNotFound || err == ErrLoginNotFound { |
| paddy@98 | 248 return Profile{}, ErrIncorrectAuth |
| paddy@98 | 249 } |
| paddy@98 | 250 return Profile{}, err |
| paddy@98 | 251 } |
| paddy@98 | 252 if profile.Compromised { |
| paddy@98 | 253 return Profile{}, ErrProfileCompromised |
| paddy@98 | 254 } |
| paddy@98 | 255 if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) { |
| paddy@98 | 256 return profile, ErrProfileLocked |
| paddy@98 | 257 } |
| paddy@98 | 258 scheme, ok := passphraseSchemes[profile.PassphraseScheme] |
| paddy@98 | 259 if !ok { |
| paddy@98 | 260 return Profile{}, ErrInvalidPassphraseScheme |
| paddy@98 | 261 } |
| paddy@98 | 262 result, err := scheme.check(profile, passphrase) |
| paddy@98 | 263 if !result { |
| paddy@98 | 264 return Profile{}, err |
| paddy@98 | 265 } |
| paddy@98 | 266 return profile, nil |
| paddy@98 | 267 } |
| paddy@98 | 268 |
| paddy@98 | 269 // CreateSessionHandler allows the user to log into their account and create their session. |
| paddy@98 | 270 func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@98 | 271 errors := []error{} |
| paddy@98 | 272 if r.Method == "POST" { |
| paddy@98 | 273 profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context) |
| paddy@98 | 274 if err == nil { |
| paddy@98 | 275 ip := r.Header.Get("X-Forwarded-For") |
| paddy@98 | 276 if ip == "" { |
| paddy@98 | 277 ip = r.RemoteAddr |
| paddy@98 | 278 } |
| paddy@132 | 279 sessionID := make([]byte, 32) |
| paddy@132 | 280 csrfToken := make([]byte, 32) |
| paddy@132 | 281 _, err = rand.Read(sessionID) |
| paddy@132 | 282 if err != nil { |
| paddy@132 | 283 log.Println("Error reading CSPRNG for session ID:", err) |
| paddy@132 | 284 w.WriteHeader(http.StatusInternalServerError) |
| paddy@132 | 285 w.Write([]byte("Internal error")) |
| paddy@132 | 286 return |
| paddy@132 | 287 } |
| paddy@132 | 288 _, err = rand.Read(csrfToken) |
| paddy@132 | 289 if err != nil { |
| paddy@132 | 290 log.Println("Error reading CSPRNG for CSRF token:", err) |
| paddy@132 | 291 w.WriteHeader(http.StatusInternalServerError) |
| paddy@132 | 292 w.Write([]byte("internal error")) |
| paddy@132 | 293 return |
| paddy@132 | 294 } |
| paddy@98 | 295 session := Session{ |
| paddy@159 | 296 ID: base64.URLEncoding.EncodeToString(sessionID), |
| paddy@98 | 297 IP: ip, |
| paddy@98 | 298 UserAgent: r.UserAgent(), |
| paddy@98 | 299 ProfileID: profile.ID, |
| paddy@98 | 300 Login: r.PostFormValue("login"), |
| paddy@98 | 301 Created: time.Now(), |
| paddy@132 | 302 Expires: time.Now().Add(time.Hour), |
| paddy@98 | 303 Active: true, |
| paddy@159 | 304 CSRFToken: base64.URLEncoding.EncodeToString(csrfToken), |
| paddy@98 | 305 } |
| paddy@98 | 306 err = context.CreateSession(session) |
| paddy@98 | 307 if err != nil { |
| paddy@98 | 308 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 309 w.Write([]byte(err.Error())) |
| paddy@98 | 310 return |
| paddy@98 | 311 } |
| paddy@132 | 312 // BUG(paddy): We really need to do a security audit on our cookies. |
| paddy@98 | 313 cookie := http.Cookie{ |
| paddy@98 | 314 Name: authCookieName, |
| paddy@98 | 315 Value: session.ID, |
| paddy@132 | 316 Expires: session.Expires, |
| paddy@98 | 317 HttpOnly: true, |
| paddy@132 | 318 Secure: context.config.secureCookie, |
| paddy@98 | 319 } |
| paddy@98 | 320 http.SetCookie(w, &cookie) |
| paddy@98 | 321 redirectTo := r.URL.Query().Get("from") |
| paddy@98 | 322 if redirectTo == "" { |
| paddy@98 | 323 redirectTo = "/" |
| paddy@98 | 324 } |
| paddy@98 | 325 http.Redirect(w, r, redirectTo, http.StatusFound) |
| paddy@98 | 326 return |
| paddy@139 | 327 } else if !isAuthError(err) { |
| paddy@98 | 328 w.WriteHeader(http.StatusInternalServerError) |
| paddy@98 | 329 w.Write([]byte(err.Error())) |
| paddy@98 | 330 return |
| paddy@98 | 331 } else { |
| paddy@98 | 332 errors = append(errors, err) |
| paddy@98 | 333 } |
| paddy@98 | 334 } |
| paddy@98 | 335 context.Render(w, loginTemplateName, map[string]interface{}{ |
| paddy@98 | 336 "errors": errors, |
| paddy@98 | 337 }) |
| paddy@98 | 338 } |
| paddy@119 | 339 |
| paddy@159 | 340 // TerminateSessionHandler allows the user to end their session before it expires. |
| paddy@159 | 341 func TerminateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@159 | 342 var errors []requestError |
| paddy@159 | 343 vars := mux.Vars(r) |
| paddy@159 | 344 if vars["id"] == "" { |
| paddy@159 | 345 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@159 | 346 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@159 | 347 return |
| paddy@159 | 348 } |
| paddy@159 | 349 id := vars["id"] |
| paddy@159 | 350 un, pw, ok := r.BasicAuth() |
| paddy@159 | 351 if !ok { |
| paddy@159 | 352 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@159 | 353 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@159 | 354 return |
| paddy@159 | 355 } |
| paddy@159 | 356 profile, err := authenticate(un, pw, context) |
| paddy@159 | 357 if err != nil { |
| paddy@159 | 358 if isAuthError(err) { |
| paddy@159 | 359 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@159 | 360 encode(w, r, http.StatusForbidden, response{Errors: errors}) |
| paddy@159 | 361 return |
| paddy@159 | 362 } |
| paddy@159 | 363 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@159 | 364 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@159 | 365 return |
| paddy@159 | 366 } |
| paddy@159 | 367 session, err := context.GetSession(id) |
| paddy@159 | 368 if err != nil { |
| paddy@159 | 369 if err == ErrSessionNotFound { |
| paddy@159 | 370 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@159 | 371 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@159 | 372 return |
| paddy@159 | 373 } |
| paddy@159 | 374 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@159 | 375 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@159 | 376 return |
| paddy@159 | 377 } |
| paddy@159 | 378 if !session.ProfileID.Equal(profile.ID) { |
| paddy@159 | 379 errors = append(errors, requestError{Slug: requestErrAccessDenied, Param: "id"}) |
| paddy@159 | 380 encode(w, r, http.StatusForbidden, response{Errors: errors}) |
| paddy@159 | 381 return |
| paddy@159 | 382 } |
| paddy@159 | 383 err = context.TerminateSession(id) |
| paddy@159 | 384 if err != nil { |
| paddy@159 | 385 if err == ErrSessionNotFound { |
| paddy@159 | 386 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@159 | 387 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@159 | 388 return |
| paddy@159 | 389 } |
| paddy@159 | 390 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@159 | 391 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@159 | 392 return |
| paddy@159 | 393 } |
| paddy@159 | 394 session.Active = false |
| paddy@159 | 395 encode(w, r, http.StatusOK, response{Sessions: []Session{session}, Errors: errors}) |
| paddy@159 | 396 } |
| paddy@159 | 397 |
| paddy@135 | 398 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) { |
| paddy@119 | 399 enc := json.NewEncoder(w) |
| paddy@119 | 400 username := r.PostFormValue("username") |
| paddy@119 | 401 password := r.PostFormValue("password") |
| paddy@135 | 402 scopes = strings.Split(r.PostFormValue("scope"), " ") |
| paddy@119 | 403 profile, err := authenticate(username, password, context) |
| paddy@119 | 404 if err != nil { |
| paddy@139 | 405 if isAuthError(err) { |
| paddy@119 | 406 w.WriteHeader(http.StatusBadRequest) |
| paddy@119 | 407 renderJSONError(enc, "invalid_grant") |
| paddy@119 | 408 return |
| paddy@119 | 409 } |
| paddy@119 | 410 w.WriteHeader(http.StatusInternalServerError) |
| paddy@119 | 411 w.Write([]byte(err.Error())) |
| paddy@119 | 412 return |
| paddy@119 | 413 } |
| paddy@119 | 414 profileID = profile.ID |
| paddy@119 | 415 valid = true |
| paddy@119 | 416 return |
| paddy@119 | 417 } |
| paddy@124 | 418 |
| paddy@124 | 419 func credentialsAuditString(r *http.Request) string { |
| paddy@124 | 420 return "credentials" |
| paddy@124 | 421 } |