Change password endpoint implemented and tested

This commit is contained in:
Daniel Krol 2022-07-06 14:02:34 -04:00
parent bce47979f6
commit 125e461d95
5 changed files with 502 additions and 13 deletions

84
server/password.go Normal file
View file

@ -0,0 +1,84 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"orblivion/lbry-id/auth"
"orblivion/lbry-id/store"
"orblivion/lbry-id/wallet"
)
type ChangePasswordRequest struct {
EncryptedWallet wallet.EncryptedWallet `json:"encryptedWallet"`
Sequence wallet.Sequence `json:"sequence"`
Hmac wallet.WalletHmac `json:"hmac"`
Email auth.Email `json:"email"`
OldPassword auth.Password `json:"oldPassword"`
NewPassword auth.Password `json:"newPassword"`
}
func (r *ChangePasswordRequest) validate() bool {
// The wallet should be here or not. Not partially here.
walletPresent := (r.EncryptedWallet != "" && r.Hmac != "" && r.Sequence > 0)
walletAbsent := (r.EncryptedWallet == "" && r.Hmac == "" && r.Sequence == 0)
return (validateEmail(r.Email) &&
r.OldPassword != "" &&
r.NewPassword != "" &&
r.OldPassword != r.NewPassword &&
(walletPresent || walletAbsent))
}
func (s *Server) changePassword(w http.ResponseWriter, req *http.Request) {
var changePasswordRequest ChangePasswordRequest
if !getPostData(w, req, &changePasswordRequest) {
return
}
var err error
if changePasswordRequest.EncryptedWallet != "" {
err = s.store.ChangePasswordWithWallet(
changePasswordRequest.Email,
changePasswordRequest.OldPassword,
changePasswordRequest.NewPassword,
changePasswordRequest.EncryptedWallet,
changePasswordRequest.Sequence,
changePasswordRequest.Hmac)
if err == store.ErrWrongSequence {
errorJson(w, http.StatusConflict, "Bad sequence number or wallet does not exist")
return
}
} else {
err = s.store.ChangePasswordNoWallet(
changePasswordRequest.Email,
changePasswordRequest.OldPassword,
changePasswordRequest.NewPassword,
)
if err == store.ErrUnexpectedWallet {
errorJson(w, http.StatusConflict, "Wallet exists; need an updated wallet when changing password")
return
}
}
if err == store.ErrWrongCredentials {
errorJson(w, http.StatusUnauthorized, "No match for email and password")
return
}
if err != nil {
internalServiceErrorJson(w, err, "Error changing password")
return
}
var changePasswordResponse struct{} // no data to respond with, but keep it JSON
var response []byte
response, err = json.Marshal(changePasswordResponse)
if err != nil {
internalServiceErrorJson(w, err, "Error generating change password response")
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, string(response))
}

350
server/password_test.go Normal file
View file

@ -0,0 +1,350 @@
package server
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"orblivion/lbry-id/auth"
"orblivion/lbry-id/store"
"orblivion/lbry-id/wallet"
)
func TestServerChangePassword(t *testing.T) {
tt := []struct {
name string
expectedStatusCode int
expectedErrorString string
// Whether we expect the call to ChangePassword*Wallet to happen
expectChangePasswordCall bool
// `new...` refers to what is being passed into the via POST request (and
// what we expect to get passed into SetWallet for the *non-error* cases
// below)
newEncryptedWallet wallet.EncryptedWallet
newSequence wallet.Sequence
newHmac wallet.WalletHmac
email auth.Email
storeErrors TestStoreFunctionsErrors
}{
{
name: "success with wallet",
expectedStatusCode: http.StatusOK,
expectChangePasswordCall: true,
newEncryptedWallet: "my-enc-wallet",
newSequence: 2,
newHmac: "my-hmac",
email: "abc@example.com",
}, {
name: "success no wallet",
expectedStatusCode: http.StatusOK,
expectChangePasswordCall: true,
email: "abc@example.com",
}, {
name: "conflict with wallet",
expectedStatusCode: http.StatusConflict,
expectedErrorString: http.StatusText(http.StatusConflict) + ": Bad sequence number or wallet does not exist",
expectChangePasswordCall: true,
newEncryptedWallet: "my-enc-wallet",
newSequence: 2,
newHmac: "my-hmac",
email: "abc@example.com",
storeErrors: TestStoreFunctionsErrors{ChangePasswordWithWallet: store.ErrWrongSequence},
}, {
name: "conflict no wallet",
expectedStatusCode: http.StatusConflict,
expectedErrorString: http.StatusText(http.StatusConflict) + ": Wallet exists; need an updated wallet when changing password",
expectChangePasswordCall: true,
email: "abc@example.com",
storeErrors: TestStoreFunctionsErrors{ChangePasswordNoWallet: store.ErrUnexpectedWallet},
}, {
name: "incorrect email with wallet",
expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": No match for email and password",
expectChangePasswordCall: true,
newEncryptedWallet: "my-enc-wallet",
newSequence: 2,
newHmac: "my-hmac",
email: "abc@example.com",
storeErrors: TestStoreFunctionsErrors{ChangePasswordWithWallet: store.ErrWrongCredentials},
}, {
name: "incorrect email no wallet",
expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": No match for email and password",
expectChangePasswordCall: true,
email: "abc@example.com",
storeErrors: TestStoreFunctionsErrors{ChangePasswordNoWallet: store.ErrWrongCredentials},
}, {
name: "validation error",
expectedStatusCode: http.StatusBadRequest,
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation",
// Just check one validation error (missing email address) to make sure
// the validate function is called. We'll check the rest of the
// validation errors in the other test below.
expectChangePasswordCall: false,
}, {
name: "db error changing password with wallet",
expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError),
expectChangePasswordCall: true,
// Putting in valid data here so it's clear that this isn't what causes
// the error
newEncryptedWallet: "my-encrypted-wallet",
newSequence: 2,
newHmac: "my-hmac",
email: "abc@example.com",
// What causes the error
storeErrors: TestStoreFunctionsErrors{ChangePasswordWithWallet: fmt.Errorf("Some random db problem")},
}, {
name: "db error changing password no wallet",
expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError),
expectChangePasswordCall: true,
email: "abc@example.com",
// What causes the error
storeErrors: TestStoreFunctionsErrors{ChangePasswordNoWallet: fmt.Errorf("Some random db problem")},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
testAuth := TestAuth{}
testStore := TestStore{Errors: tc.storeErrors}
s := Server{&testAuth, &testStore}
// Whether we passed in wallet fields (these test cases should be passing
// in all of them or none of them, so we only test EncryptedWallet). This
// determines whether we expect a call to ChangePasswordWithWallet (as
// opposed to ChangePasswordNoWallet).
withWallet := (tc.newEncryptedWallet != "")
const oldPassword = "old password"
const newPassword = "new password"
requestBody := []byte(
fmt.Sprintf(`{
"encryptedWallet": "%s",
"sequence": %d,
"hmac": "%s",
"email": "%s",
"oldPassword": "%s",
"newPassword": "%s"
}`, tc.newEncryptedWallet, tc.newSequence, tc.newHmac, tc.email, oldPassword, newPassword),
)
req := httptest.NewRequest(http.MethodPost, PathPassword, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
s.changePassword(w, req)
body, _ := ioutil.ReadAll(w.Body)
expectStatusCode(t, w, tc.expectedStatusCode)
expectErrorString(t, body, tc.expectedErrorString)
if tc.expectedErrorString == "" && string(body) != "{}" {
t.Errorf("Expected change password response to be \"{}\": result: %+v", string(body))
}
if tc.expectChangePasswordCall {
if withWallet {
// Called ChangePasswordWithWallet with the expected parameters
if want, got := (ChangePasswordWithWalletCall{
EncryptedWallet: tc.newEncryptedWallet,
Sequence: tc.newSequence,
Hmac: tc.newHmac,
Email: tc.email,
OldPassword: oldPassword,
NewPassword: newPassword,
}), testStore.Called.ChangePasswordWithWallet; want != got {
t.Errorf("Store.ChangePasswordWithWallet called with: expected %+v, got %+v", want, got)
}
// Did *not* call ChangePasswordNoWallet
if want, got := (ChangePasswordNoWalletCall{}), testStore.Called.ChangePasswordNoWallet; want != got {
t.Errorf("Store.ChangePasswordNoWallet unexpectly called with: %+v", got)
}
} else {
// Called ChangePasswordNoWallet with the expected parameters
if want, got := (ChangePasswordNoWalletCall{
Email: tc.email,
OldPassword: oldPassword,
NewPassword: newPassword,
}), testStore.Called.ChangePasswordNoWallet; want != got {
t.Errorf("Store.ChangePasswordNoWallet called with: expected %+v, got %+v", want, got)
}
// Did *not* call ChangePasswordWithWallet
if want, got := (ChangePasswordWithWalletCall{}), testStore.Called.ChangePasswordWithWallet; want != got {
t.Errorf("Store.ChangePasswordWithWallet unexpectly called with: %+v", got)
}
}
} else {
if want, got := (ChangePasswordWithWalletCall{}), testStore.Called.ChangePasswordWithWallet; want != got {
t.Errorf("Store.ChangePasswordWithWallet unexpectly called with: %+v", got)
}
if want, got := (ChangePasswordNoWalletCall{}), testStore.Called.ChangePasswordNoWallet; want != got {
t.Errorf("Store.ChangePasswordNoWallet unexpectly called with: %+v", got)
}
}
})
}
}
func TestServerValidateChangePasswordRequest(t *testing.T) {
changePasswordRequest := ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 2,
Email: "abc@example.com",
OldPassword: "123",
NewPassword: "456",
}
if !changePasswordRequest.validate() {
t.Errorf("Expected valid ChangePasswordRequest with wallet fields to successfully validate")
}
changePasswordRequest = ChangePasswordRequest{
Email: "abc@example.com",
OldPassword: "123",
NewPassword: "456",
}
if !changePasswordRequest.validate() {
t.Errorf("Expected valid ChangePasswordRequest without wallet fields to successfully validate")
}
tt := []struct {
changePasswordRequest ChangePasswordRequest
failureDescription string
}{
{
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 2,
Email: "abc-example.com",
OldPassword: "123",
NewPassword: "456",
},
"Expected WalletRequest with invalid email to not successfully validate",
}, {
// Note that Golang's email address parser, which I use, will accept
// "Abc <abc@example.com>" so we need to make sure to avoid accepting it. See
// the implementation.
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 2,
Email: "Abc <abc@example.com>",
OldPassword: "123",
NewPassword: "456",
},
"Expected WalletRequest with email with unexpected formatting to not successfully validate",
}, {
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 2,
OldPassword: "123",
NewPassword: "456",
},
"Expected WalletRequest with missing email to not successfully validate",
}, {
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 2,
Email: "abc@example.com",
NewPassword: "456",
},
"Expected WalletRequest with missing old password to not successfully validate",
}, {
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 2,
Email: "abc@example.com",
OldPassword: "123",
},
"Expected WalletRequest with missing new password to not successfully validate",
}, {
ChangePasswordRequest{
Hmac: "my-hmac",
Sequence: 2,
Email: "abc@example.com",
OldPassword: "123",
NewPassword: "456",
},
"Expected WalletRequest with missing encrypted wallet (but with other wallet fields present) to not successfully validate",
}, {
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Sequence: 2,
Email: "abc@example.com",
OldPassword: "123",
NewPassword: "456",
},
"Expected WalletRequest with missing hmac (but with other wallet fields present) to not successfully validate",
}, {
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 0,
Email: "abc@example.com",
OldPassword: "123",
NewPassword: "456",
},
"Expected WalletRequest with sequence < 1 (but with other wallet fields present) to not successfully validate",
}, {
ChangePasswordRequest{
EncryptedWallet: "my-encrypted-wallet",
Hmac: "my-hmac",
Sequence: 2,
Email: "abc@example.com",
OldPassword: "123",
NewPassword: "123",
},
"Expected WalletRequest with password that does not change to not successfully validate",
},
}
for _, tc := range tt {
if tc.changePasswordRequest.validate() {
t.Errorf(tc.failureDescription)
}
}
}

View file

@ -17,6 +17,7 @@ const PathPrefix = "/api/" + ApiVersion
const PathAuthToken = PathPrefix + "/auth/full" const PathAuthToken = PathPrefix + "/auth/full"
const PathRegister = PathPrefix + "/signup" const PathRegister = PathPrefix + "/signup"
const PathPassword = PathPrefix + "/password"
const PathWallet = PathPrefix + "/wallet" const PathWallet = PathPrefix + "/wallet"
type Server struct { type Server struct {
@ -178,6 +179,7 @@ func (s *Server) Serve() {
http.HandleFunc(PathAuthToken, s.getAuthToken) http.HandleFunc(PathAuthToken, s.getAuthToken)
http.HandleFunc(PathWallet, s.handleWallet) http.HandleFunc(PathWallet, s.handleWallet)
http.HandleFunc(PathRegister, s.register) http.HandleFunc(PathRegister, s.register)
http.HandleFunc(PathPassword, s.changePassword)
fmt.Println("Serving at localhost:8090") fmt.Println("Serving at localhost:8090")
http.ListenAndServe("localhost:8090", nil) http.ListenAndServe("localhost:8090", nil)

View file

@ -35,23 +35,42 @@ type SetWalletCall struct {
Hmac wallet.WalletHmac Hmac wallet.WalletHmac
} }
type ChangePasswordNoWalletCall struct {
Email auth.Email
OldPassword auth.Password
NewPassword auth.Password
}
type ChangePasswordWithWalletCall struct {
EncryptedWallet wallet.EncryptedWallet
Sequence wallet.Sequence
Hmac wallet.WalletHmac
Email auth.Email
OldPassword auth.Password
NewPassword auth.Password
}
// Whether functions are called, and sometimes what they're called with // Whether functions are called, and sometimes what they're called with
type TestStoreFunctionsCalled struct { type TestStoreFunctionsCalled struct {
SaveToken auth.TokenString SaveToken auth.TokenString
GetToken auth.TokenString GetToken auth.TokenString
GetUserId bool GetUserId bool
CreateAccount bool CreateAccount bool
SetWallet SetWalletCall SetWallet SetWalletCall
GetWallet bool GetWallet bool
ChangePasswordWithWallet ChangePasswordWithWalletCall
ChangePasswordNoWallet ChangePasswordNoWalletCall
} }
type TestStoreFunctionsErrors struct { type TestStoreFunctionsErrors struct {
SaveToken error SaveToken error
GetToken error GetToken error
GetUserId error GetUserId error
CreateAccount error CreateAccount error
SetWallet error SetWallet error
GetWallet error GetWallet error
ChangePasswordWithWallet error
ChangePasswordNoWallet error
} }
type TestStore struct { type TestStore struct {
@ -110,6 +129,38 @@ func (s *TestStore) GetWallet(userId auth.UserId) (encryptedWallet wallet.Encryp
return return
} }
func (s *TestStore) ChangePasswordWithWallet(
email auth.Email,
oldPassword auth.Password,
newPassword auth.Password,
encryptedWallet wallet.EncryptedWallet,
sequence wallet.Sequence,
hmac wallet.WalletHmac,
) (err error) {
s.Called.ChangePasswordWithWallet = ChangePasswordWithWalletCall{
EncryptedWallet: encryptedWallet,
Sequence: sequence,
Hmac: hmac,
Email: email,
OldPassword: oldPassword,
NewPassword: newPassword,
}
return s.Errors.ChangePasswordWithWallet
}
func (s *TestStore) ChangePasswordNoWallet(
email auth.Email,
oldPassword auth.Password,
newPassword auth.Password,
) (err error) {
s.Called.ChangePasswordNoWallet = ChangePasswordNoWalletCall{
Email: email,
OldPassword: oldPassword,
NewPassword: newPassword,
}
return s.Errors.ChangePasswordNoWallet
}
// expectStatusCode: A helper to call in functions that test that request // expectStatusCode: A helper to call in functions that test that request
// handlers responded with a certain status code. Cuts down on noise. // handlers responded with a certain status code. Cuts down on noise.
func expectStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedStatusCode int) { func expectStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedStatusCode int) {

View file

@ -42,7 +42,9 @@ type StoreInterface interface {
SetWallet(auth.UserId, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error SetWallet(auth.UserId, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error) GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error)
GetUserId(auth.Email, auth.Password) (auth.UserId, error) GetUserId(auth.Email, auth.Password) (auth.UserId, error)
CreateAccount(auth.Email, auth.Password) (err error) CreateAccount(auth.Email, auth.Password) error
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password) error
} }
type Store struct { type Store struct {