Let people change their own password, and operators reset a lost one
CI / test (push) Failing after 1m11s
CI / test (push) Failing after 1m11s
The account system shipped without either, which made the first forgotten password an unanswerable support request. Changing it requires the current one. A session is what an attacker gets from a borrowed laptop or a stolen cookie; the password is what they do not have, and without the check the first thing they would do is take the account. Every session is closed, including the browser doing the changing — leaving the others open would mean a password change does nothing about the reason someone is changing it. A new session is then opened for the browser in front of us, so the person who just proved their password is not thrown back to the sign-in page. That response also carries a new CSRF cookie, so the forms it renders take the new token rather than the one the request arrived with; there is a test that posts a form from that exact page. Guessing the current password is bounded the same way sign-in is, because someone holding a stolen session should not get unlimited attempts at the password that would let them keep it. There is no self-service reset: this gateway sends no email, and a reset link with nowhere to send it is not a reset. `llmgw users reset-password` is the operator's answer, and it closes their sessions too — if someone lost their password, the reason might be that somebody else has it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
216fc9bb03
commit
79e459d9ff
10 files changed
+437
-24
No files matched your search
+41
-1
@@ -32,7 +32,7 @@ func newUsersCmd() *cobra.Command {
|
||||
"Stop an account working, keeping it and its history"),
|
||||
newUserStateCmd(open, "reinstate", store.UserActive,
|
||||
"Let a suspended account work again"),
|
||||
newUserCreateCmd(open))
|
||||
newUserCreateCmd(open), newUserResetPasswordCmd(open))
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -142,6 +142,46 @@ func newUserStateCmd(open userOpener, verb string, state store.UserState, short
|
||||
}
|
||||
}
|
||||
|
||||
func newUserResetPasswordCmd(open userOpener) *cobra.Command {
|
||||
var password string
|
||||
cmd := &cobra.Command{
|
||||
Use: "reset-password <user-id>",
|
||||
Short: "Set a new password for someone who lost theirs",
|
||||
Long: "There is no self-service reset, because this gateway sends no email and\n" +
|
||||
"a reset link with nowhere to send it is not a reset. Set one here, tell\n" +
|
||||
"them out of band, and have them change it from their profile.\n\n" +
|
||||
"Every session they have open is closed: if they lost the password, the\n" +
|
||||
"reason might be that somebody else has it.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if password == "" {
|
||||
return fmt.Errorf("--password is required")
|
||||
}
|
||||
s, err := open(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
hash, err := account.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.SetPassword(cmd.Context(), args[0], hash); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.DeleteUserSessions(cmd.Context(), args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "password set for %s\n", args[0])
|
||||
cmd.PrintErrln("Their sessions were closed. Their API keys still work.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&password, "password", "", "the new password")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newUserCreateCmd(open userOpener) *cobra.Command {
|
||||
var project, password string
|
||||
cmd := &cobra.Command{
|
||||
|
||||
Reference in new issue
Block a user