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

Conversation

martonp
Copy link
Contributor

@martonp martonp commented Jul 2, 2021

This PR adds a checkbox to the login page that gives the user the option to persist their password for the session. When selected, all the password fields, other than disable account, will no longer be displayed.

When the checkbox is selected, the password is encrypted with a random key, and the encrypted password is stored in a cache in core. The key to decrypt the password is put into a cookie. Whenever a request that requires a password is made with an empty password field, the webserver uses the key from the cookie to retrieve the password from core.

Closes #946.

Copy link
Member

@buck54321 buck54321 left a comment

Choose a reason for hiding this comment

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

First pass. Looks good conceptually, though I've suggested some architectural changes.

client/webserver/api.go Outdated Show resolved Hide resolved
client/webserver/api.go Outdated Show resolved Hide resolved
client/webserver/api.go Outdated Show resolved Hide resolved
client/webserver/api.go Outdated Show resolved Hide resolved
client/webserver/site/src/html/forms.tmpl Outdated Show resolved Hide resolved
client/webserver/site/src/html/wallets.tmpl Outdated Show resolved Hide resolved
client/core/core.go Outdated Show resolved Hide resolved
client/webserver/site/src/js/settings.js Outdated Show resolved Hide resolved
client/webserver/webserver.go Outdated Show resolved Hide resolved
client/core/core.go Outdated Show resolved Hide resolved
@@ -396,7 +403,9 @@ func TestAPIWithdraw(t *testing.T) {
return resp.OK
}

body = &withdrawForm{}
body = &withdrawForm{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is needed because resolvePass will return an error if this is an empty string.

@martonp
Copy link
Contributor Author

martonp commented Jul 15, 2021

@buck54321 This is ready for another review.

Copy link
Member

@buck54321 buck54321 left a comment

Choose a reason for hiding this comment

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

Partial review. Lemme know what you think about these notes. I promise to review faster now.

client/webserver/http.go Outdated Show resolved Hide resolved
client/webserver/site/src/html/forms.tmpl Outdated Show resolved Hide resolved
client/webserver/site/src/html/forms.tmpl Outdated Show resolved Hide resolved
client/webserver/site/src/html/login.tmpl Outdated Show resolved Hide resolved
client/webserver/webserver.go Outdated Show resolved Hide resolved
client/webserver/webserver_test.go Show resolved Hide resolved
@martonp
Copy link
Contributor Author

martonp commented Jul 28, 2021

@buck54321 It's ready for another look.

In the registration workflow (where the page is not reloaded between forms) State.passwordIsCached is being used. I kept using the template arguments on the other pages though since it makes the code much more simple. There is always a page reload when going from the login/registration pages to every other page.

@chappjc chappjc added UI labels Aug 2, 2021
@chappjc chappjc added this to the 0.3 milestone Aug 3, 2021
@chappjc chappjc self-requested a review August 5, 2021 02:25
@chappjc
Copy link
Member

chappjc commented Aug 5, 2021

@martonp Please rebase and resolve when you get a chance.

@chappjc
Copy link
Member

chappjc commented Aug 5, 2021

@martonp Please try to avoid merging from master to get the branch up-to-date. Instead, rebase on master.

It is tricky to get rid of the merge commit when you synchronized with master @ 16da72f. I'm squashing this branch down to a single commit with 16da72f as it's base like:

git checkout rememberPW
git reset --hard 16da72f5e3af049db315400c68b87d7957c15bfa
git merge --squash HEAD@{1}
git commit -m "Add ability to remember password at login."

Then it can be rebased on master a bit more easily since it's just one commit now (git rebase master).

Although I see that the conflicts are gonna be tricky to resolve, particularly in forms.js, so thank you.

Copy link
Member

@buck54321 buck54321 left a comment

Choose a reason for hiding this comment

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

Looking really good. I want to give it one more look, but it's testing well. Just a couple of little things for now.

Comment on lines 609 to 610
passwordIsCached := s.isPasswordCached(r)
s.deauth()
Copy link
Member

Choose a reason for hiding this comment

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

Maybe switch the order of these two lines so that the comment is right above the line it applies to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I do that then I won't know if the password was cached before I called deauth. I can put the comment between the two lines.

Comment on lines 41 to 42
<div{{if not .UserInfo.Authed}} class="d-hide"{{end}}>
<div {{if not .UserInfo.Authed}}class="d-hide"{{end}}>
Copy link
Member

Choose a reason for hiding this comment

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

Now, it will be <div > instead of <div>. What's the purpose? (one other place too)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a mistake.. it was red in my IDE the previous way, and I thought they were equivalent.

Comment on lines 332 to 333
this.fields.unlockErr.textContent = msg
Doc.show(this.fields.unlockErr)
Copy link
Member

Choose a reason for hiding this comment

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

this.setError(msg)

Comment on lines 11 to 12
<label for="rememberPass" class="pl-1 mb-1">Remember my password</label>
<input type="checkbox" id="rememberPass">
Copy link
Member

Choose a reason for hiding this comment

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

To be consistent with other forms, put the input before the label, and add class="form-check-input" to the checkbox. I think you'll need to wrap everything in a class="pl-4" div too, but not certain.

Also looks like you need some padding between the password field and the remember-password input.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks way better now.

*/
async openWallet (assetID) {
if (!State.passwordIsCached()) {
this.showOpen.bind(this)(assetID)
Copy link
Member

Choose a reason for hiding this comment

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

Don't need to .bind(this) here.

if (app.checkResponse(res)) {
this.openWalletSuccess.bind(this)()
} else {
this.showOpen.bind(this)(assetID, `Error opening wallet: ${res.msg}`)
Copy link
Member

Choose a reason for hiding this comment

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

Don't need to .bind(this) here.

Comment on lines 431 to 441
cookie, err := r.Cookie(pwKeyCK)
switch {
case err == nil:
sessionKey, err := hex.DecodeString(cookie.Value)
if err != nil {
return nil, err
}
return sessionKey, nil
default:
return nil, err
}
Copy link
Member

Choose a reason for hiding this comment

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

We actually want to see theses errors. How about this?

switch {
case err != nil:
	...
case errors.Is(err, http.ErrNoCookie):
    return nil, nil
default:
	return nil, err
}

and then in getCachedPasswordUsingRequest, you return nil, errNoCachedPW on the condition pwKeyBlob == nil, but not err != nil

Comment on lines 457 to 458
// getCachedPassword takes authToken passed to and the key returned from
// cacheAppPassword to retrieve and decrypt the app password.
Copy link
Member

Choose a reason for hiding this comment

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

// getCachedPassword retrieves the cached password for the user identified by authToken and
// presenting the specified key in their cookies.


crypter, err := encrypt.Deserialize(key, cachedPassword.SerializedCrypter)
if err != nil {
return nil, fmt.Errorf("error deserializing crypter: %v", err)
Copy link
Member

Choose a reason for hiding this comment

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

Might as well %w.


pw, err := crypter.Decrypt(cachedPassword.EncryptedPass)
if err != nil {
return nil, fmt.Errorf("error decrypting password: %v", err)
Copy link
Member

Choose a reason for hiding this comment

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

Might as well %w.

srv *http.Server
html *templates
indent bool
mtx sync.RWMutex
Copy link
Member

Choose a reason for hiding this comment

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

We should really rename this to authMtx or something, shouldn't we?

Comment on lines 670 to 675
pass, err := s.resolvePass(form.Pass, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %v", err))
return
}
coin, err := s.core.Withdraw(pass, form.AssetID, form.Value, form.Address)
Copy link
Member

Choose a reason for hiding this comment

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

We should always require the password to be entered manually for withdraws.

Comment on lines 156 to 158
<div class="fs16 px-4 text-center {{if $passwordIsCached}}d-hide{{end}}">Authorize the withdraw with your app password.</div>
<div class="d-flex px-4 mt-3 {{if $passwordIsCached}}justify-content-end{{end}}">
<div class="col-12 p-0 {{if $passwordIsCached}}d-hide{{end}}">
Copy link
Member

Choose a reason for hiding this comment

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

Should always require password for withdraws.

@martonp
Copy link
Contributor Author

martonp commented Aug 16, 2021

@buck54321 I agree with all the comments, and I've updated them all.

Copy link
Member

@buck54321 buck54321 left a comment

Choose a reason for hiding this comment

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

Please increment the cache busters in bodybuilder.js, and this is good with me.

srv *http.Server
html *templates
indent bool
authMtx sync.RWMutex
Copy link
Member

Choose a reason for hiding this comment

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

Would prefer a newline to segregate these guarded fields with their mutex. Shoulda been that way previously, so might as well change it now.

if err != nil {
return nil, err
}
if pwKeyBlob == nil && err == nil {
Copy link
Member

Choose a reason for hiding this comment

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

err is guaranteed to be nil here, so this half of the condition is redundant.

@@ -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.

Comment on lines 135 to 136
authTokens map[string]bool
cachedPasswords map[string]*cachedPassword
Copy link
Member

Choose a reason for hiding this comment

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

The keys for these maps are the same, right (both auth tokens)? Don't worry about refactoring at this stage, but it suggests a single map would be adequate if the logic around access to the map were updated to reflect new semantics (found would indicate known auth token regardless of value stored including nil, while non-nil value would indicate a cached password). Rather than change anything, I just request a simple trailing comment on this delc line to doco the map key

cachedPasswords map[string]*cachedPassword // cached passwords keyed by auth token

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that makes sense, I'll do another PR for this.

@chappjc chappjc merged commit 7a0c387 into decred:master Aug 19, 2021
@martonp martonp deleted the rememberPW branch December 20, 2022 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

multi: allow persistent encryption key
3 participants