fix(web): keep generated passwords out of page source
This commit is contained in:
@@ -38,6 +38,7 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("/admin/logout", s.handleAdminWebLogout)
|
||||
s.mux.HandleFunc("/admin/action", s.handleAdminWebAction)
|
||||
s.mux.HandleFunc("/admin/password-result", s.handleAdminPasswordResult)
|
||||
s.mux.HandleFunc("/admin/password-result/secret", s.handleAdminPasswordResultSecret)
|
||||
s.mux.HandleFunc("/admin/dashboard", s.handleAdminWeb)
|
||||
s.mux.HandleFunc("/admin/users", s.handleAdminWeb)
|
||||
s.mux.HandleFunc("/admin/create-user", s.handleAdminCreateUserWeb)
|
||||
|
||||
@@ -36,3 +36,11 @@ document.addEventListener("click", async function (event) {
|
||||
// The downloadable JSON link remains available when clipboard access is unavailable.
|
||||
}
|
||||
});
|
||||
|
||||
const oneTimeSecret = document.querySelector("[data-one-time-secret-url]");
|
||||
if (oneTimeSecret) {
|
||||
fetch(oneTimeSecret.dataset.oneTimeSecretUrl, { method: "POST", credentials: "same-origin", headers: { "X-CSRF-Token": oneTimeSecret.dataset.csrfToken } })
|
||||
.then(async function (response) { if (!response.ok) throw new Error("one-time secret unavailable"); return response.json(); })
|
||||
.then(function (data) { oneTimeSecret.querySelector(".one-time-secret").textContent = data.password; })
|
||||
.catch(function () { oneTimeSecret.querySelector(".one-time-secret").textContent = "—"; });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{{define "content"}}
|
||||
<section class="admin-shell">{{template "admin_nav" .}}<div class="admin-content narrow-content">
|
||||
<p class="eyebrow">{{t .Locale "admin.resultTitle"}}</p><h1>{{t .Locale "admin.resetPassword"}}</h1>
|
||||
<section class="card panel warning"><p>{{t .Locale "admin.oneTimePasswordNotice"}}</p><code class="one-time-secret">{{.OneTimeSecret}}</code><p>{{t .Locale "admin.oneTimePasswordHint"}}</p></section>
|
||||
<section class="card panel warning" data-one-time-secret-url="/admin/password-result/secret" data-csrf-token="{{.CSRF}}"><p>{{t .Locale "admin.oneTimePasswordNotice"}}</p><code class="one-time-secret" aria-live="polite">{{t .Locale "common.loading"}}</code><p>{{t .Locale "admin.oneTimePasswordHint"}}</p></section>
|
||||
<a class="button secondary" href="/admin/users">{{t .Locale "admin.users"}}</a>
|
||||
</div></section>
|
||||
{{end}}
|
||||
|
||||
@@ -62,18 +62,30 @@ func (s *Server) handleAdminPasswordResult(w http.ResponseWriter, r *http.Reques
|
||||
if !s.requireAdminCookie(w, r) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store, max-age=0")
|
||||
s.renderPage(w, r, "admin_password_result", webPage{Title: "admin.resetPassword", Admin: true, AdminPage: "users"})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminPasswordResultSecret(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
if !s.requireAdminMutation(w, r) {
|
||||
return
|
||||
}
|
||||
cookie, err := r.Cookie("admin_session")
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
|
||||
jsonErrCode(w, http.StatusForbidden, "session_invalid", "administrator session is required")
|
||||
return
|
||||
}
|
||||
secret := s.takeAdminOneTimeSecret(cookie.Value)
|
||||
if secret == "" {
|
||||
http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
|
||||
jsonErrCode(w, http.StatusGone, "one_time_secret_expired", "one-time password is no longer available")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store, max-age=0")
|
||||
s.renderPage(w, r, "admin_password_result", webPage{Title: "admin.resetPassword", Admin: true, AdminPage: "users", OneTimeSecret: secret})
|
||||
jsonOK(w, map[string]string{"password": secret})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminVaultDetail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -650,15 +650,26 @@ func TestAdminPasswordResetShowsGeneratedSecretOnce(t *testing.T) {
|
||||
resultRequest.AddCookie(&http.Cookie{Name: "admin_session", Value: adminToken})
|
||||
resultResponse := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(resultResponse, resultRequest)
|
||||
if resultResponse.Code != http.StatusOK || !strings.Contains(resultResponse.Header().Get("Cache-Control"), "no-store") || !strings.Contains(resultResponse.Body.String(), "one-time-secret") {
|
||||
if resultResponse.Code != http.StatusOK || !strings.Contains(resultResponse.Header().Get("Cache-Control"), "no-store") || !strings.Contains(resultResponse.Body.String(), "data-one-time-secret-url") {
|
||||
t.Fatalf("password result=%d headers=%v body=%s", resultResponse.Code, resultResponse.Header(), resultResponse.Body.String())
|
||||
}
|
||||
secondRequest := httptest.NewRequest(http.MethodGet, "/admin/password-result", nil)
|
||||
secretRequest := httptest.NewRequest(http.MethodPost, "/admin/password-result/secret", nil)
|
||||
secretRequest.Header.Set("X-CSRF-Token", csrf)
|
||||
secretRequest.AddCookie(&http.Cookie{Name: "admin_session", Value: adminToken})
|
||||
secretRequest.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
|
||||
secretResponse := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(secretResponse, secretRequest)
|
||||
if secretResponse.Code != http.StatusOK || !strings.Contains(secretResponse.Header().Get("Cache-Control"), "no-store") || !strings.Contains(secretResponse.Body.String(), `"password"`) {
|
||||
t.Fatalf("secret response=%d headers=%v body=%s", secretResponse.Code, secretResponse.Header(), secretResponse.Body.String())
|
||||
}
|
||||
secondRequest := httptest.NewRequest(http.MethodPost, "/admin/password-result/secret", nil)
|
||||
secondRequest.Header.Set("X-CSRF-Token", csrf)
|
||||
secondRequest.AddCookie(&http.Cookie{Name: "admin_session", Value: adminToken})
|
||||
secondRequest.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
|
||||
secondResponse := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(secondResponse, secondRequest)
|
||||
if secondResponse.Code != http.StatusSeeOther || secondResponse.Header().Get("Location") != "/admin/users" {
|
||||
t.Fatalf("second password result=%d %q", secondResponse.Code, secondResponse.Header().Get("Location"))
|
||||
if secondResponse.Code != http.StatusGone {
|
||||
t.Fatalf("second password result=%d: %s", secondResponse.Code, secondResponse.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ type webPage struct {
|
||||
FormAction string
|
||||
BackURL string
|
||||
Token string
|
||||
OneTimeSecret string
|
||||
Admin bool
|
||||
UserName string
|
||||
Email string
|
||||
|
||||
Reference in New Issue
Block a user