store.ChangePasswordNoWallet implement and test

This commit is contained in:
Daniel Krol 2022-07-05 17:12:14 -04:00
parent 41b65d08ab
commit 02a5b9ce24
2 changed files with 204 additions and 6 deletions

View file

@ -8,6 +8,9 @@ import (
"orblivion/lbry-id/wallet"
)
// It involves both wallet and account tables. Should it go in wallet_test.go
// or account_test.go? Decided to just make it its own file.
func TestStoreChangePasswordSuccess(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
@ -36,8 +39,8 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
sequence := wallet.Sequence(2)
hmac := wallet.WalletHmac("my-hmac-2")
if err := s.ChangePassword(email, oldPassword, newPassword, encryptedWallet, sequence, hmac); err != nil {
t.Errorf("ChangePassword: unexpected error: %+v", err)
if err := s.ChangePasswordWithWallet(email, oldPassword, newPassword, encryptedWallet, sequence, hmac); err != nil {
t.Errorf("ChangePasswordWithWallet: unexpected error: %+v", err)
}
expectAccountMatch(t, &s, email, newPassword)
@ -130,8 +133,8 @@ func TestStoreChangePasswordErrors(t *testing.T) {
submittedOldPassword := oldPassword + tc.oldPasswordSuffix // Possibly make it the wrong password
newPassword := oldPassword + auth.Password("_new") // Possibly make the new password different (as it should be)
if err := s.ChangePassword(submittedEmail, submittedOldPassword, newPassword, newEncryptedWallet, tc.sequence, newHmac); err != tc.expectedError {
t.Errorf("ChangePassword: unexpected value for err. want: %+v, got: %+v", tc.expectedError, err)
if err := s.ChangePasswordWithWallet(submittedEmail, submittedOldPassword, newPassword, newEncryptedWallet, tc.sequence, newHmac); err != tc.expectedError {
t.Errorf("ChangePasswordWithWallet: unexpected value for err. want: %+v, got: %+v", tc.expectedError, err)
}
// The password and wallet didn't change, the token didn't get deleted.
@ -148,3 +151,119 @@ func TestStoreChangePasswordErrors(t *testing.T) {
})
}
}
func TestStoreChangePasswordNoWalletSuccess(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
userId, email, oldPassword := makeTestUser(t, &s)
token := auth.TokenString("my-token")
_, err := s.db.Exec(
"INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) VALUES(?,?,?,?,?)",
token, userId, "my-dev-id", "*", time.Now().UTC().Add(time.Hour*24*14),
)
if err != nil {
t.Fatalf("Error creating token")
}
newPassword := oldPassword + auth.Password("_new")
if err := s.ChangePasswordNoWallet(email, oldPassword, newPassword); err != nil {
t.Errorf("ChangePasswordNoWallet: unexpected error: %+v", err)
}
expectAccountMatch(t, &s, email, newPassword)
expectWalletNotExists(t, &s, userId)
expectTokenNotExists(t, &s, token)
}
func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
tt := []struct {
name string
hasWallet bool
emailSuffix auth.Email
oldPasswordSuffix auth.Password
expectedError error
}{
{
name: "wrong email",
hasWallet: false, // we don't have the wallet, as expected for this function
emailSuffix: auth.Email("_wrong"), // the email is *incorrect*
oldPasswordSuffix: auth.Password(""), // the password is correct
expectedError: ErrWrongCredentials,
}, {
name: "wrong old password",
hasWallet: false, // we don't have the wallet, as expected for this function
emailSuffix: auth.Email(""), // the email is correct
oldPasswordSuffix: auth.Password("_wrong"), // the old password is *incorrect*
expectedError: ErrWrongCredentials,
}, {
name: "unexpected wallet",
hasWallet: true, // we have a wallet which we shouldn't have at this point
emailSuffix: auth.Email(""), // the email is correct
oldPasswordSuffix: auth.Password(""), // the password is correct
expectedError: ErrUnexpectedWallet,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
userId, email, oldPassword := makeTestUser(t, &s)
expiration := time.Now().UTC().Add(time.Hour * 24 * 14)
authToken := auth.AuthToken{
Token: auth.TokenString("my-token"),
DeviceId: auth.DeviceId("my-dev-id"),
UserId: userId,
Scope: auth.AuthScope("*"),
Expiration: &expiration,
}
_, err := s.db.Exec(
"INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) VALUES(?,?,?,?,?)",
authToken.Token, authToken.UserId, authToken.DeviceId, authToken.Scope, authToken.Expiration,
)
if err != nil {
t.Fatalf("Error creating token")
}
// Only for error case
encryptedWallet := wallet.EncryptedWallet("my-enc-wallet-old")
hmac := wallet.WalletHmac("my-hmac-old")
sequence := wallet.Sequence(1)
if tc.hasWallet {
_, err := s.db.Exec(
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac) VALUES(?,?,?,?)",
userId, encryptedWallet, sequence, hmac,
)
if err != nil {
t.Fatalf("Error creating test wallet")
}
}
submittedEmail := email + tc.emailSuffix // Possibly make it the wrong email
submittedOldPassword := oldPassword + tc.oldPasswordSuffix // Possibly make it the wrong password
newPassword := oldPassword + auth.Password("_new") // Possibly make the new password different (as it should be)
if err := s.ChangePasswordNoWallet(submittedEmail, submittedOldPassword, newPassword); err != tc.expectedError {
t.Errorf("ChangePasswordNoWallet: unexpected value for err. want: %+v, got: %+v", tc.expectedError, err)
}
// The password and wallet (if any) didn't change, the token didn't get
// deleted. This tests the transaction rollbacks in particular, given the
// errors that are at a couple different stages of the txn, triggered by
// these tests.
expectAccountMatch(t, &s, email, oldPassword)
if tc.hasWallet {
expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac)
} else {
expectWalletNotExists(t, &s, userId)
}
expectTokenExists(t, &s, authToken)
})
}
}

View file

@ -26,7 +26,8 @@ var (
// return ErrWrongSequence to begin with. But think about it.
ErrNoWallet = fmt.Errorf("Wallet does not exist for this user at this sequence")
ErrWrongSequence = fmt.Errorf("Wallet could not be updated to this sequence")
ErrUnexpectedWallet = fmt.Errorf("Wallet unexpectedly exist for this user")
ErrWrongSequence = fmt.Errorf("Wallet could not be updated to this sequence")
ErrDuplicateEmail = fmt.Errorf("Email already exists for this user")
ErrDuplicateAccount = fmt.Errorf("User already has an account")
@ -385,7 +386,7 @@ func (s *Store) CreateAccount(email auth.Email, password auth.Password) (err err
// Also delete all auth tokens to force clients to update their root password
// to get a new token. This prevents other clients from posting a wallet
// encrypted with the old key.
func (s *Store) ChangePassword(
func (s *Store) ChangePasswordWithWallet(
email auth.Email,
oldPassword auth.Password,
newPassword auth.Password,
@ -463,3 +464,81 @@ func (s *Store) ChangePassword(
_, err = tx.Exec("DELETE FROM auth_tokens WHERE user_id=?", userId)
return
}
// Change password, but with no wallet currently saved. Since there's no
// wallet saved, there's no wallet to update. The encryption key is moot.
//
// Also delete all auth tokens to force clients to update their root password
// to get a new token. This prevents other clients from posting a wallet
// encrypted with the old key.
func (s *Store) ChangePasswordNoWallet(
email auth.Email,
oldPassword auth.Password,
newPassword auth.Password,
) (err error) {
var userId auth.UserId
tx, err := s.db.Begin()
if err != nil {
return
}
// Lots of error conditions. Just defer this. However, we need to make sure to
// make sure the variable `err` is set to the error before we return, instead
// of doing `return <error>`.
endTxn := func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}
defer endTxn()
err = tx.QueryRow(
"SELECT user_id from accounts WHERE email=? AND password=?",
email, oldPassword.Obfuscate(),
).Scan(&userId)
if err == sql.ErrNoRows {
err = ErrWrongCredentials
return
}
if err != nil {
return
}
res, err := tx.Exec(
"UPDATE accounts SET password=? WHERE user_id=?",
newPassword.Obfuscate(), userId,
)
if err != nil {
return
}
numRows, err := res.RowsAffected()
if err != nil {
return
}
if numRows != 1 {
// Very unexpected error!
err = fmt.Errorf("Password failed to update")
return
}
// Assert we have no wallet for this version of the password change function.
var dummy string
err = tx.QueryRow("SELECT 1 FROM wallets WHERE user_id=?", userId).Scan(&dummy)
if err != sql.ErrNoRows {
if err == nil {
// We expected no rows
err = ErrUnexpectedWallet
return
}
// Some other error
return
}
// Don't care how many I delete here. Might even be zero. No login token while
// changing password seems plausible.
_, err = tx.Exec("DELETE FROM auth_tokens WHERE user_id=?", userId)
return
}