Add an Account page for sign-in methods and signed-in devices
/web/account, offered to everyone, lists every browser and client signed in as you, with when each was last used, and signs any of them out, or all but the current one. It sets up, replaces and removes the second factor, and holds the password form, moved from the vault page. Administrators can sign a user out everywhere from the People page. The API gains GET and DELETE /api/v1/auth/sessions and DELETE /api/v1/auth/totp. Sessions are named by a digest of their token, never the token, and record when they were last used. Replacing or removing an active second factor now needs the password and a current code. Server version 0.10.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
318bd68908
commit
f8f634fbf7
+13
-35
@@ -20,8 +20,9 @@ const webCookie = "keeporg_web"
|
||||
|
||||
// webSession is a signed-in browser.
|
||||
type webSession struct {
|
||||
user *store.User
|
||||
csrf string
|
||||
user *store.User
|
||||
csrf string
|
||||
token string
|
||||
}
|
||||
|
||||
// webUser resolves the session cookie, or nil when the browser is not
|
||||
@@ -31,7 +32,10 @@ func (s *Server) webUser(r *http.Request) *webSession {
|
||||
if err != nil || c.Value == "" {
|
||||
return nil
|
||||
}
|
||||
var out *webSession
|
||||
var (
|
||||
out *webSession
|
||||
seen time.Time
|
||||
)
|
||||
s.st.Read(func(st *store.State) {
|
||||
sess := st.Sessions[c.Value]
|
||||
if sess == nil || time.Now().After(sess.ExpiresAt) {
|
||||
@@ -39,9 +43,13 @@ func (s *Server) webUser(r *http.Request) *webSession {
|
||||
}
|
||||
if u := st.Users[sess.UserID]; u != nil && !u.Disabled {
|
||||
cp := *u
|
||||
out = &webSession{user: &cp, csrf: sess.CSRF}
|
||||
out = &webSession{user: &cp, csrf: sess.CSRF, token: c.Value}
|
||||
seen = sess.LastSeenAt
|
||||
}
|
||||
})
|
||||
if out != nil {
|
||||
s.markSeen(c.Value, seen)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -115,7 +123,7 @@ func (s *Server) handleWebSignIn(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token = s.newSession(st, u.ID, "web").Token
|
||||
token = s.newBrowserSession(st, u.ID, r).Token
|
||||
return nil
|
||||
})
|
||||
switch {
|
||||
@@ -187,36 +195,6 @@ func (s *Server) handleWebCreateVault(w http.ResponseWriter, r *http.Request, se
|
||||
"with the password you just set.")
|
||||
}
|
||||
|
||||
func (s *Server) handleWebChangePassword(w http.ResponseWriter, r *http.Request, sess *webSession) {
|
||||
next := r.FormValue("new_password")
|
||||
if next != r.FormValue("new_password2") {
|
||||
s.renderVaults(w, sess, "The two new passwords do not match.", "")
|
||||
return
|
||||
}
|
||||
cookie, _ := r.Cookie(webCookie)
|
||||
keep := ""
|
||||
if cookie != nil {
|
||||
keep = cookie.Value
|
||||
}
|
||||
err := s.st.Update(func(st *store.State) error {
|
||||
return changePassword(st, sess.user.Username, r.FormValue("current_password"),
|
||||
r.FormValue("totp"), next, keep)
|
||||
})
|
||||
switch {
|
||||
case errors.Is(err, errTOTPRequired):
|
||||
s.renderVaults(w, sess, "Enter your one-time code to change the password.", "")
|
||||
case errors.Is(err, errTOTPInvalid):
|
||||
s.renderVaults(w, sess, "That one-time code was rejected.", "")
|
||||
case errors.Is(err, errBadCredentials):
|
||||
s.renderVaults(w, sess, "The current password is wrong.", "")
|
||||
case err != nil:
|
||||
s.renderVaults(w, sess, err.Error(), "")
|
||||
default:
|
||||
s.renderVaults(w, sess, "",
|
||||
"Password changed. Other sessions on this account were signed out.")
|
||||
}
|
||||
}
|
||||
|
||||
// webDeleteVault is what the confirmation page shows before a vault goes.
|
||||
type webDeleteVault struct {
|
||||
ID string
|
||||
|
||||
Reference in New Issue
Block a user