Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi: add ability to persist password on UI login #1119

Merged
merged 5 commits into from Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
164 changes: 133 additions & 31 deletions client/webserver/api.go
Expand Up @@ -4,6 +4,7 @@
package webserver

import (
"encoding/hex"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -45,7 +46,12 @@ func (s *WebServer) apiPreRegister(w http.ResponseWriter, r *http.Request) {
return
}
cert := []byte(form.Cert)
exchangeInfo, paid, err := s.core.PreRegister(form.Addr, form.Password, cert)
pass, err := s.resolvePass(form.Password, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
exchangeInfo, paid, err := s.core.PreRegister(form.Addr, pass, cert)
if err != nil {
s.writeAPIError(w, err)
return
Expand Down Expand Up @@ -97,11 +103,15 @@ func (s *WebServer) apiRegister(w http.ResponseWriter, r *http.Request) {
s.writeAPIError(w, errors.New("No Decred wallet"))
return
}

_, err := s.core.Register(&core.RegisterForm{
pass, err := s.resolvePass(reg.Password, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
_, err = s.core.Register(&core.RegisterForm{
Addr: reg.Addr,
Cert: []byte(reg.Cert),
AppPass: reg.Password,
AppPass: pass,
Fee: reg.Fee,
})
if err != nil {
Expand Down Expand Up @@ -130,8 +140,13 @@ func (s *WebServer) apiNewWallet(w http.ResponseWriter, r *http.Request) {
s.writeAPIError(w, fmt.Errorf("already have a wallet for %s", unbip(form.AssetID)))
return
}
pass, err := s.resolvePass(form.AppPW, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
// Wallet does not exist yet. Try to create it.
err := s.core.CreateWallet(form.AppPW, form.Pass, &core.WalletForm{
err = s.core.CreateWallet(pass, form.Pass, &core.WalletForm{
AssetID: form.AssetID,
Config: form.Config,
})
Expand All @@ -156,7 +171,12 @@ func (s *WebServer) apiOpenWallet(w http.ResponseWriter, r *http.Request) {
s.writeAPIError(w, fmt.Errorf("No wallet for %d -> %s", form.AssetID, unbip(form.AssetID)))
return
}
err := s.core.OpenWallet(form.AssetID, form.Pass)
pass, err := s.resolvePass(form.Pass, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
err = s.core.OpenWallet(form.AssetID, pass)
if err != nil {
s.writeAPIError(w, fmt.Errorf("error unlocking %s wallet: %w", unbip(form.AssetID), err))
return
Expand Down Expand Up @@ -220,7 +240,12 @@ func (s *WebServer) apiTrade(w http.ResponseWriter, r *http.Request) {
return
}
r.Close = true
ord, err := s.core.Trade(form.Pass, form.Order)
pass, err := s.resolvePass(form.Pass, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
ord, err := s.core.Trade(pass, form.Order)
if err != nil {
s.writeAPIError(w, fmt.Errorf("error placing order: %w", err))
return
Expand All @@ -244,7 +269,12 @@ func (s *WebServer) apiAccountExport(w http.ResponseWriter, r *http.Request) {
return
}
r.Close = true
account, err := s.core.AccountExport(form.Pass, form.Host)
pass, err := s.resolvePass(form.Pass, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
account, err := s.core.AccountExport(pass, form.Host)
if err != nil {
s.writeAPIError(w, fmt.Errorf("error exporting account: %w", err))
return
Expand Down Expand Up @@ -291,7 +321,12 @@ func (s *WebServer) apiAccountImport(w http.ResponseWriter, r *http.Request) {
return
}
r.Close = true
err := s.core.AccountImport(form.Pass, form.Account)
pass, err := s.resolvePass(form.Pass, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
err = s.core.AccountImport(pass, form.Account)
if err != nil {
s.writeAPIError(w, fmt.Errorf("error importing account: %w", err))
return
Expand Down Expand Up @@ -325,7 +360,12 @@ func (s *WebServer) apiCancel(w http.ResponseWriter, r *http.Request) {
if !readPost(w, r, form) {
return
}
err := s.core.Cancel(form.Pass, form.OrderID)
pass, err := s.resolvePass(form.Pass, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
err = s.core.Cancel(pass, form.OrderID)
if err != nil {
s.writeAPIError(w, fmt.Errorf("error cancelling order %s: %w", form.OrderID, err))
return
Expand Down Expand Up @@ -362,7 +402,7 @@ func (s *WebServer) apiInit(w http.ResponseWriter, r *http.Request) {
s.writeAPIError(w, fmt.Errorf("initialization error: %w", err))
return
}
s.actuallyLogin(w, r, &loginForm{Pass: init.Pass})
s.actuallyLogin(w, r, &loginForm{Pass: init.Pass, RememberPass: init.RememberPass})
}

// apiIsInitialized is the handler for the '/isinitialized' request.
Expand Down Expand Up @@ -398,13 +438,8 @@ func (s *WebServer) apiLogout(w http.ResponseWriter, r *http.Request) {
// sessions to login again.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls update comments at deauth call sites to reflect it's expanded role w.r.t. cached passwords as well as the auth tokens.

s.deauth()

http.SetCookie(w, &http.Cookie{
Name: authCK,
Path: "/",
Value: "",
Expires: time.Unix(0, 0),
SameSite: http.SameSiteStrictMode,
})
clearCookie(authCK, w)
clearCookie(pwKeyCK, w)

response := struct {
OK bool `json:"ok"`
Expand Down Expand Up @@ -567,6 +602,24 @@ func (s *WebServer) apiChangeAppPass(w http.ResponseWriter, r *http.Request) {
return
}

passwordIsCached := s.isPasswordCached(r)
// Force other sessions to login again. Without this, any sessions that
// had a cached password will no longer work. However, we assign a new auth
// token and cache the new password (if it was previously cached) for this
// session.
s.deauth()
authToken := s.authorize()
setCookie(authCK, authToken, w)
if passwordIsCached {
key, err := s.cacheAppPassword(form.NewAppPW, authToken)
if err != nil {
log.Errorf("unable to cache password: %v", err)
clearCookie(pwKeyCK, w)
} else {
setCookie(pwKeyCK, hex.EncodeToString(key), w)
}
}

writeJSON(w, simpleAck(), s.indent)
}

Expand All @@ -586,9 +639,13 @@ func (s *WebServer) apiReconfig(w http.ResponseWriter, r *http.Request) {
if !readPost(w, r, form) {
return
}

pass, err := s.resolvePass(form.AppPW, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
// Update wallet settings.
err := s.core.ReconfigureWallet(form.AppPW, form.NewWalletPW, form.AssetID,
err = s.core.ReconfigureWallet(pass, form.NewWalletPW, form.AssetID,
form.Config)
if err != nil {
s.writeAPIError(w, fmt.Errorf("reconfig error: %w", err))
Expand Down Expand Up @@ -678,7 +735,12 @@ func (s *WebServer) apiMaxSell(w http.ResponseWriter, r *http.Request) {

// apiActuallyLogin logs the user in.
func (s *WebServer) actuallyLogin(w http.ResponseWriter, r *http.Request, login *loginForm) {
loginResult, err := s.core.Login(login.Pass)
pass, err := s.resolvePass(login.Pass, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
loginResult, err := s.core.Login(pass)
if err != nil {
s.writeAPIError(w, fmt.Errorf("login error: %w", err))
return
Expand All @@ -687,16 +749,19 @@ func (s *WebServer) actuallyLogin(w http.ResponseWriter, r *http.Request, login
user := extractUserInfo(r)
if !user.Authed {
authToken := s.authorize()
http.SetCookie(w, &http.Cookie{
Name: authCK,
Value: authToken,
Path: "/",
// The client should only send the cookie with first-party requests.
// Cross-site requests should not include the auth cookie.
// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1
SameSite: http.SameSiteStrictMode,
// Secure: false, // while false we require SameSite set
})
setCookie(authCK, authToken, w)
if login.RememberPass {
key, err := s.cacheAppPassword(pass, authToken)
if err != nil {
s.writeAPIError(w, fmt.Errorf("login error: %v", err))
return
}
setCookie(pwKeyCK, hex.EncodeToString(key), w)
} else {
// If dexc was shutdown and restarted, the old pw key cookie might
// need to be cleared.
clearCookie(pwKeyCK, w)
}
}

writeJSON(w, struct {
Expand Down Expand Up @@ -739,3 +804,40 @@ func (s *WebServer) writeAPIError(w http.ResponseWriter, err error) {
log.Error(err.Error())
writeJSON(w, resp, s.indent)
}

// setCookie sets the value of a cookie in the http response.
func setCookie(name, value string, w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: name,
Path: "/",
Value: value,
SameSite: http.SameSiteStrictMode,
})
}

// clearCookie removes a cookie in the http response.
func clearCookie(name string, w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: name,
Path: "/",
Value: "",
Expires: time.Unix(0, 0),
SameSite: http.SameSiteStrictMode,
})
}

// resolvePass returns the appPW if it has a value, but if not, it attempts
// to retrieve the cached password using the information in cookies.
func (s *WebServer) resolvePass(appPW []byte, r *http.Request) ([]byte, error) {
if len(appPW) > 0 {
return appPW, nil
}
cachedPass, err := s.getCachedPasswordUsingRequest(r)
if err != nil {
if errors.Is(err, errNoCachedPW) {
return nil, fmt.Errorf("app pass cannot be empty")
}
return nil, fmt.Errorf("error retrieving cached pw: %w", err)
}
return cachedPass, nil
}
9 changes: 5 additions & 4 deletions client/webserver/middleware.go
Expand Up @@ -39,10 +39,11 @@ func (s *WebServer) securityMiddleware(next http.Handler) http.Handler {
func (s *WebServer) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxKeyUserInfo, &userInfo{
User: s.core.User(),
Authed: s.isAuthed(r),
DarkMode: extractBooleanCookie(r, darkModeCK, true),
ShowPopups: extractBooleanCookie(r, popupsCK, true),
User: s.core.User(),
Authed: s.isAuthed(r),
PasswordIsCached: s.isPasswordCached(r),
DarkMode: extractBooleanCookie(r, darkModeCK, true),
ShowPopups: extractBooleanCookie(r, popupsCK, true),
})
next.ServeHTTP(w, r.WithContext(ctx))
})
Expand Down
17 changes: 10 additions & 7 deletions client/webserver/site/src/html/forms.tmpl
Expand Up @@ -96,7 +96,7 @@
<div class="fs14 px-1 mb-1">Your app password is always required when performing sensitive wallet operations.</div>
<input type="password" class="form-control select" id="uwAppPass" autocomplete="off">
</div>
<div class="d-flex justify-content-end mt-4">
<div id="submitUnlockDiv" class="d-flex justify-content-end mt-4">
<button id="submitUnlock" type="submit" class="col-8 justify-content-center fs15 bg2 selected">Unlock</button>
</div>
<div class="fs15 pt-3 text-center d-hide errcolor" id="unlockErr"></div>
Expand Down Expand Up @@ -140,13 +140,14 @@
{{end}}

{{define "confirmRegistrationForm"}}
{{$passwordIsCached := .UserInfo.PasswordIsCached}}
<div class="bg2 px-2 py-1 text-center position-relative fs18">
Confirm Registration
<div class="form-closer hoverbg"><span class="ico-cross"></span></div>
</div>
<div class="p-4">
<div class="fs16">
Enter your app password to confirm DEX registration.
<span {{if $passwordIsCached}}class="d-hide"{{end}}>Enter your app password to confirm DEX registration.</span>
When you submit this form, <span id="feeDisplay"></span> DCR will be spent from your Decred wallet to pay
registration fees.
</div>
Expand All @@ -157,7 +158,7 @@
{{- /* this will change when lot size is a market setting, not an asset setting */ -}}
</div>
<hr class="dashed my-4">
<div>
<div {{if $passwordIsCached}}class="d-hide"{{end}}>
<label for="appPass" class="pl-1 mb-1">Password</label>
<input type="password" class="form-control select" id="appPass" autocomplete="current-password">
</div>
Expand Down Expand Up @@ -216,6 +217,7 @@
{{end}}

{{define "authorizeAccountImportForm"}}
{{$passwordIsCached := .UserInfo.PasswordIsCached}}
<div class="bg2 px-2 py-1 text-center position-relative fs18">
Authorize Import
<div class="form-closer hoverbg"><span class="ico-cross"></span></div>
Expand All @@ -234,7 +236,7 @@
<span class="ml-3 pointer" id="addAccount"><span class="ico-textfile mr-1"></span> load from file</span>
</div>
</div>
<div>
<div {{if $passwordIsCached}}class="d-hide"{{end}}>
<label for="importAccountAppPass" class="pl-1 mb-1">Password</label>
<input type="password" class="form-control select" id="importAccountAppPass" autocomplete="current-password">
</div>
Expand Down Expand Up @@ -271,6 +273,7 @@
{{end}}

{{define "cancelOrderForm"}}
{{$passwordIsCached := .UserInfo.PasswordIsCached}}
<div class="bg2 px-2 py-1 text-center fs18 position-relative">
Cancel Order
<div class="form-closer hoverbg"><span class="ico-cross"></span></div>
Expand All @@ -282,12 +285,12 @@
The remaining amount may change before the cancel order is matched.
</div>
<hr class="dashed mt-2">
<div class="d-flex flex-row align-items-end pb-4 px-3">
<div class="col-12 p-0">
<div class="d-flex flex-row align-items-end {{if $passwordIsCached}}justify-content-end{{end}} pb-4 px-3">
<div class="col-12 p-0 {{if $passwordIsCached}}d-hide{{end}}">
<label for="cancelPass" class="pt-3 pl-1 mb-0">Password</label>
<input type="password" class="form-control select" id="cancelPass" autocomplete="off">
</div>
<div class="col-12 py-1 pl-5">
<div class="col-12 py-1 {{if not $passwordIsCached}}pl-5{{end}}">
<button id="cancelSubmit" type="button" class="w-100 fs15 bg2 selected">Submit</button>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions client/webserver/site/src/html/login.tmpl
Expand Up @@ -8,6 +8,10 @@
<div class="pb-2">
<label for="pw" class="pl-1 mb-1">Password</label>
<input type="password" class="form-control select" id="pw" autocomplete="current-password">
<div class="pl-4 pt-2">
<input class="form-check-input" type="checkbox" id="rememberPass">
<label for="rememberPass" class="pl-1">Remember my password</label>
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button id="submit" type="button" class="col-8 justify-content-center fs15 bg2 selected">Submit</button>
Expand Down