Fix single sign-on from the web, and make passwords changeable
Why: two questions about operating this turned up two gaps. "Where do I get the admin password" had no answer once the line scrolled out of the log, and "how do I set up OIDC" ran straight into a sign-in link that could not work. What: - /oidc/login with no parameters now signs a browser in and ends in a session cookie. It previously required the user code of a waiting client, so the link on the web sign-in page always failed. With a code it still approves a device, and the two paths no longer overlap: an empty user code matches no pending device. - POST /api/v1/auth/password, `keeporg passwd`, and a form on /web/vaults. Changing a password requires the current one, and the one-time code when a second factor is enrolled, and revokes that account's other sessions. Until now nothing could change a password, which made the advice printed at first start impossible to follow. - keeporgd -reset-admin, for when that first password was not kept: a new one printed once, any second factor cleared, sessions revoked. It writes the state document directly and says to stop the server first. - The client read stdin through a new buffered reader per prompt, so the first prompt swallowed the rest and anything asking twice could not be scripted. One shared reader now. A prompt for a password being set no longer reads $KEEPORG_PASSWORD either; that variable holds the password you sign in with, and taking it here would have set the new password to the old one. - HANDOVER gains a first-sign-in section and an OIDC setup section covering the redirect URI, the config block, both entry points and how an identity becomes an account. Proved: OIDC now has tests, against a stub provider that serves discovery, authorization, token and userinfo. They cover the web sign-in that was broken, device approval, linking an existing account by username, the refusal when auto_create is off, and a forged callback state. Eight more cover the password change. Checked by hand with the real binaries: the first-start password logs once and works, -reset-admin replaces it while the old one stops working, and `keeporg passwd` changes it from a pipe. Server 0.5.0, desktop clients 0.3.0.
This commit is contained in:
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -301,6 +302,65 @@ func (s *Server) handleDeviceApprove(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// changePassword replaces a user's password after proving the current one.
|
||||
// It is the one place a password changes, so the rules live here: the
|
||||
// current password is required even though the caller already holds a
|
||||
// session, the second factor is required when one is enrolled, and every
|
||||
// other session of that user is revoked, because a password change is what
|
||||
// someone does when they think a session is no longer theirs.
|
||||
func changePassword(st *store.State, username, current, totpCode, next, keepToken string) error {
|
||||
u, err := checkPassword(st, username, current, totpCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(next) < MinPasswordLen {
|
||||
return fmt.Errorf("the new password must be at least %d characters", MinPasswordLen)
|
||||
}
|
||||
if next == current {
|
||||
return errors.New("the new password is the same as the old one")
|
||||
}
|
||||
hash, err := secret.Hash(next)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.PassHash = hash
|
||||
for token, sess := range st.Sessions {
|
||||
if sess.UserID == u.ID && token != keepToken {
|
||||
delete(st.Sessions, token)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request, u *store.User) {
|
||||
var req struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
TOTP string `json:"totp"`
|
||||
}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
err := s.st.Update(func(st *store.State) error {
|
||||
return changePassword(st, u.Username, req.CurrentPassword, req.TOTP,
|
||||
req.NewPassword, bearer(r))
|
||||
})
|
||||
switch {
|
||||
case errors.Is(err, errTOTPRequired):
|
||||
fail(w, http.StatusUnauthorized, "totp_required", "second factor required")
|
||||
case errors.Is(err, errTOTPInvalid):
|
||||
fail(w, http.StatusUnauthorized, "totp_invalid", "second factor rejected")
|
||||
case errors.Is(err, errBadCredentials):
|
||||
fail(w, http.StatusUnauthorized, "bad_credentials", "the current password is wrong")
|
||||
case err != nil:
|
||||
fail(w, http.StatusBadRequest, "bad_request", err.Error())
|
||||
default:
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"status": "password changed; other sessions on this account were signed out",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- invites ----
|
||||
|
||||
func (s *Server) handleInvitePage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user