add password change form

This commit is contained in:
Henry 2021-05-11 10:16:35 +02:00
parent 3d9c567cf6
commit 653d0926f7
10 changed files with 162 additions and 10 deletions

1
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.4.2
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472
github.com/mattevans/pwned-passwords v0.3.0 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0
github.com/nicksnyder/go-i18n/v2 v2.1.2

5
go.sum
View File

@ -264,6 +264,8 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattevans/pwned-passwords v0.3.0 h1:PFUAQXHH6NVugTiQ3Uh/iUY5dUljtEmzdg2kE8a7cXI=
github.com/mattevans/pwned-passwords v0.3.0/go.mod h1:waUnV5nlikMlUqnjQtFV+DAgFPUQNPabvMGv8NG2IaQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -318,6 +320,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
@ -333,6 +336,8 @@ github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736 h1:C9bEdTfu5QY+TIf
github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=

View File

@ -9,10 +9,12 @@ import (
)
var (
// ErrRedirect decide to not render a page during the controller
ErrNotAuthorized = errors.New("rooms/web: not authorized")
ErrDenied = errors.New("rooms: this key has been banned")
ErrInsecurePassword = errors.New("room: password was found on the insecure password list of have-i-been-pwned")
ErrPasswordMissmatch = errors.New("room: the entered password did not match the repeated one")
)
type ErrNotFound struct{ What string }

View File

@ -13,6 +13,7 @@ import (
"github.com/go-kit/kit/log/level"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
hibp "github.com/mattevans/pwned-passwords"
"github.com/russross/blackfriday/v2"
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
@ -37,6 +38,8 @@ var HTMLTemplates = []string{
"landing/about.tmpl",
"alias.tmpl",
"change-member-password.tmpl",
"invite/consumed.tmpl",
"invite/facade.tmpl",
"invite/facade-fallback.tmpl",
@ -231,6 +234,10 @@ func New(
})),
)
// Init the have-i-been-pwned client for insecure password checks.
const storeExpiry = 1 * time.Hour
hibpClient := hibp.NewClient(storeExpiry)
// this router is a bit of a qurik
// TODO: explain problem between gorilla/mux named routers and authentication
mainMux := &http.ServeMux{}
@ -295,6 +302,73 @@ func New(
)
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
m.Get(router.MembersChangePasswordForm).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if members.FromContext(req.Context()) == nil {
r.Error(w, req, http.StatusUnauthorized, weberrs.ErrNotAuthorized)
return
}
var pageData = make(map[string]interface{})
pageData[csrf.TemplateTag] = csrf.TemplateField(req)
pageData["Flashes"], err = flashHelper.GetAll(w, req)
if err != nil {
r.Error(w, req, http.StatusInternalServerError, err)
return
}
// TODO: add resetToken to render
err = r.Render(w, req, "change-member-password.tmpl", http.StatusOK, pageData)
if err != nil {
r.Error(w, req, http.StatusInternalServerError, err)
}
})
m.Get(router.MembersChangePassword).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
redirectURL := req.Header.Get("Referer")
if redirectURL == "" {
http.Error(w, "TODO: add correct redirect handling", http.StatusInternalServerError)
return
}
err := req.ParseForm()
if err != nil {
r.Error(w, req, http.StatusInternalServerError, err)
return
}
repeat := req.FormValue("repeat-password")
newpw := req.FormValue("new-password")
if newpw != repeat {
flashHelper.AddError(w, req, weberrs.ErrPasswordMissmatch)
http.Redirect(w, req, redirectURL, http.StatusSeeOther)
return
}
if len(newpw) < 10 {
flashHelper.AddError(w, req, fmt.Errorf("password too short"))
http.Redirect(w, req, redirectURL, http.StatusSeeOther)
return
}
isPwned, err := hibpClient.Pwned.Compromised(newpw)
if err != nil {
r.Error(w, req, http.StatusInternalServerError, fmt.Errorf("have-i-been-pwned client failed: %w", err))
return
}
if isPwned {
flashHelper.AddError(w, req, weberrs.ErrInsecurePassword)
http.Redirect(w, req, redirectURL, http.StatusSeeOther)
return
}
fmt.Fprintln(w, "password looks okay!")
// TODO: update password db
})
// handle setting language
m.Get(router.CompleteSetLanguage).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
lang := req.FormValue("lang")

View File

@ -55,6 +55,9 @@ AuthFallbackTitle = "Passwort anmelden"
AuthFallbackWelcome = "Eine Anmeldung mit Benutzername und Passwort ist nur möglich, wenn der Administrator Ihnen eines gegeben hat, da wir die Benutzerregistrierung nicht unterstützen."
AuthFallbackInstruct = "Diese Methode ist ein akzeptabler Fallback, wenn Sie einen Benutzernamen und ein Passwort haben."
AuthFallbackPasswordChangeFormTitle = "Change Password"
AuthFallbackPasswordChangeWelcome = "Here you can change your fallback password. Make sure it's longer then 10 characters and that they match."
# general dashboard stuff
#########################
@ -113,6 +116,9 @@ AdminMemberDetailsRole = "Berechtigungsstufe"
AdminMemberDetailsAliases = "Aliase"
AdminMemberDetailsAliasRevoke = "Widerrufen"
AdminMemberDetailsAliasRevoked = "Alias wurde widerrufen"
AdminMemberDetailsInitiatePasswordChange = "Zurücksetzen des Plan-B Passworts"
AdminMemberDetailsChangePassword = "Passwort ändern"
AdminMemberDetailsCreatePasswordResetLink = "Reset Link erzeugen"
AdminMemberDetailsExclusion = "Ausschluss aus diesem Raum"
AdminMemberDetailsRemove = "Mitglied entfernen"

View File

@ -62,6 +62,9 @@ AuthFallbackTitle = "Password sign-in"
AuthFallbackWelcome = "Signing in with username and password is only possible if the administrator has given you one, because we do not support user registration."
AuthFallbackInstruct = "This method is an acceptable fallback, if you have a username and password."
AuthFallbackPasswordChangeFormTitle = "Change Password"
AuthFallbackPasswordChangeWelcome = "Here you can change your fallback password. Make sure it's longer then 10 characters and that they match."
# general dashboard stuff
#########################
@ -120,6 +123,9 @@ AdminMemberDetailsRole = "Permission level"
AdminMemberDetailsAliases = "Aliases"
AdminMemberDetailsAliasRevoke = "Revoke"
AdminMemberDetailsAliasRevoked = "Alias was revoked"
AdminMemberDetailsInitiatePasswordChange = "Re-set Fallback password"
AdminMemberDetailsChangePassword = "Change password"
AdminMemberDetailsCreatePasswordResetLink = "Create password reset link"
AdminMemberDetailsExclusion = "Exclusion from this room"
AdminMemberDetailsRemove = "Remove member"

View File

@ -23,11 +23,12 @@ const (
AdminMemberDetails = "admin:member:details"
AdminMembersOverview = "admin:members:overview"
AdminMembersAdd = "admin:members:add"
AdminMembersChangeRole = "admin:members:change-role"
AdminMembersRemoveConfirm = "admin:members:remove:confirm"
AdminMembersRemove = "admin:members:remove"
AdminMembersOverview = "admin:members:overview"
AdminMembersAdd = "admin:members:add"
AdminMembersChangeRole = "admin:members:change-role"
AdminMembersCreateFallbackReset = "admin:members:create-password-reset-link"
AdminMembersRemoveConfirm = "admin:members:remove:confirm"
AdminMembersRemove = "admin:members:remove"
AdminInvitesOverview = "admin:invites:overview"
AdminInvitesRevokeConfirm = "admin:invites:revoke:confirm"
@ -67,6 +68,7 @@ func Admin(m *mux.Router) *mux.Router {
m.Path("/members").Methods("GET").Name(AdminMembersOverview)
m.Path("/members/add").Methods("POST").Name(AdminMembersAdd)
m.Path("/members/change-role").Methods("POST").Name(AdminMembersChangeRole)
m.Path("/members/create-fallback-reset-link").Methods("POST").Name(AdminMembersCreateFallbackReset)
m.Path("/members/remove/confirm").Methods("GET").Name(AdminMembersRemoveConfirm)
m.Path("/members/remove").Methods("POST").Name(AdminMembersRemove)

View File

@ -9,7 +9,6 @@ import (
// constant names for the named routes
const (
CompleteIndex = "complete:index"
CompleteAbout = "complete:about"
CompleteNoticeShow = "complete:notice:show"
CompleteNoticeList = "complete:notice:list"
@ -22,6 +21,9 @@ const (
CompleteInviteFacadeFallback = "complete:invite:accept:fallback"
CompleteInviteInsertID = "complete:invite:insert-id"
CompleteInviteConsume = "complete:invite:consume"
MembersChangePasswordForm = "members:change-password:form"
MembersChangePassword = "members:change-password"
)
// CompleteApp constructs a mux.Router containing the routes for batch Complete html frontend
@ -32,10 +34,12 @@ func CompleteApp() *mux.Router {
Admin(m.PathPrefix("/admin").Subrouter())
m.Path("/").Methods("GET").Name(CompleteIndex)
m.Path("/about").Methods("GET").Name(CompleteAbout)
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
m.Path("/members/change-password").Methods("GET").Name(MembersChangePasswordForm)
m.Path("/members/change-password").Methods("POST").Name(MembersChangePassword)
m.Path("/join").Methods("GET").Name(CompleteInviteFacade)
m.Path("/join-fallback").Methods("GET").Name(CompleteInviteFacadeFallback)
m.Path("/join-manually").Methods("GET").Name(CompleteInviteInsertID)

View File

@ -9,7 +9,7 @@
<label class="mt-2 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsRole"}}</label>
{{ $user := is_logged_in }}
{{ $aliasBelongsToUser := eq $user.PubKey.Ref .Member.PubKey.Ref }}
{{ $viewerIsSameAsMember := eq $user.PubKey.Ref .Member.PubKey.Ref }}
{{ if member_is_elevated }}
<details class="mb-8 self-start w-40" id="change-role">
<summary class="px-3 py-1 rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
@ -69,7 +69,7 @@
>{{.Name}}</a>
</div>
{{ if or member_is_elevated $aliasBelongsToUser }}
{{ if or member_is_elevated $viewerIsSameAsMember }}
<a
href="{{urlTo "admin:aliases:revoke:confirm" "id" .ID}}"
class="w-20 py-2 text-sm text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
@ -79,6 +79,24 @@
</div>
{{end}}
{{ if $viewerIsSameAsMember }}
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsInitiatePasswordChange"}}</label>
<a
id="change-password"
href="{{urlTo "members:change-password:form" "id" .Member.ID}}"
class="mb-8 self-start shadow rounded px-3 py-1 text-yellow-600 ring-1 ring-yellow-400 bg-white hover:bg-yellow-600 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-400 cursor-pointer"
>{{i18n "AdminMemberDetailsChangePassword"}}</a>
{{ else if member_is_elevated }}
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsInitiatePasswordChange"}}</label>
<form method="POST" action="{{urlTo "admin:members:create-password-reset-link" "id" .Member.ID}}">
{{ .csrfField }}
<input type="submit"
class="mb-8 self-start shadow rounded px-3 py-1 text-yellow-600 ring-1 ring-yellow-400 bg-white hover:bg-yellow-600 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-400 cursor-pointer"
value="{{i18n "AdminMemberDetailsCreatePasswordResetLink"}}">
</form>
{{ end }}
{{ if member_is_elevated }}
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsExclusion"}}</label>
<a

View File

@ -0,0 +1,34 @@
{{ define "title" }}{{ i18n "AuthFallbackPasswordChangeFormTitle" }}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<span id="welcome" class="text-center mt-8">{{i18n "AuthFallbackPasswordChangeWelcome"}}</span>
{{ template "flashes" . }}
<form
id="change-password"
action="{{urlTo "members:change-password"}}"
method="POST"
class="flex flex-col items-center self-stretch"
>
{{.csrfField}}
<input type="hidden" name="member-id" value="member-id"}}>
<input
type="password"
name="new-password"
class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">
<input
type="password"
name="repeat-password"
class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">
<button
type="submit"
class="my-8 w-32 shadow rounded px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
>{{i18n "GenericSubmit"}}</button>
</form>
</div>
{{ end }}