Verify account endpoint

This commit is contained in:
Daniel Krol 2022-07-26 16:36:57 -04:00
parent 0c6964df0e
commit f15875c4a6
8 changed files with 169 additions and 16 deletions

View file

@ -20,6 +20,7 @@ type KDFKey string // KDF output
type ClientSaltSeed string // part of client-side KDF input along with root password
type ServerSalt string // server-side KDF input for accounts
type TokenString string
type VerifyTokenString string
type AuthScope string
const ScopeFull = AuthScope("*")

View file

@ -104,3 +104,54 @@ modes:
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, string(response))
}
// TODO - There's probably a struct-based solution here like with POST/PUT.
// We could put that struct up top as well.
func getVerifyParams(req *http.Request) (token auth.VerifyTokenString, err error) {
tokenSlice, hasTokenSlice := req.URL.Query()["verifyToken"]
if !hasTokenSlice || tokenSlice[0] == "" {
err = fmt.Errorf("Missing verifyToken parameter")
}
if err == nil {
token = auth.VerifyTokenString(tokenSlice[0])
}
return
}
func (s *Server) verify(w http.ResponseWriter, req *http.Request) {
if !getGetData(w, req) {
return
}
token, paramsErr := getVerifyParams(req)
if paramsErr != nil {
// In this specific case, the error is limited to values that are safe to
// give to the user.
errorJson(w, http.StatusBadRequest, paramsErr.Error())
return
}
err := s.store.VerifyAccount(token)
if err == store.ErrNoTokenForUser {
errorJson(w, http.StatusForbidden, "Verification token not found or expired")
return
} else if err != nil {
internalServiceErrorJson(w, err, "Error verifying account")
return
}
var verifyResponse struct{}
response, err := json.Marshal(verifyResponse)
if err != nil {
internalServiceErrorJson(w, err, "Error generating verify response")
return
}
fmt.Fprintf(w, string(response))
}

View file

@ -11,6 +11,7 @@ import (
"strings"
"testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/store"
)
@ -251,3 +252,88 @@ func TestServerValidateRegisterRequest(t *testing.T) {
t.Errorf("Expected RegisterRequest with clientSaltSeed with a non-hex string to return an appropriate error")
}
}
func TestServerVerifyAccountSuccess(t *testing.T) {
testStore := TestStore{TestVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
s := Server{&TestAuth{}, &testStore, &TestEnv{}}
req := httptest.NewRequest(http.MethodGet, PathVerify, nil)
q := req.URL.Query()
q.Add("verifyToken", string(testStore.TestVerifyTokenString))
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
s.verify(w, req)
body, _ := ioutil.ReadAll(w.Body)
expectStatusCode(t, w, http.StatusOK)
if string(body) != "{}" {
t.Errorf("Expected register response to be \"{}\": result: %+v", string(body))
}
if !testStore.Called.VerifyAccount {
t.Errorf("Expected Store.VerifyAccount to be called")
}
}
func TestServerVerifyAccountErrors(t *testing.T) {
tt := []struct {
name string
token auth.VerifyTokenString
expectedStatusCode int
expectedErrorString string
expectedCallVerifyAccount bool
storeErrors TestStoreFunctionsErrors
}{
{
name: "missing token",
token: "",
expectedStatusCode: http.StatusBadRequest,
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Missing verifyToken parameter",
expectedCallVerifyAccount: false,
},
{
name: "not found token", // including expired
token: "abcd1234abcd1234abcd1234abcd1234",
expectedStatusCode: http.StatusForbidden,
expectedErrorString: http.StatusText(http.StatusForbidden) + ": Verification token not found or expired",
storeErrors: TestStoreFunctionsErrors{VerifyAccount: store.ErrNoTokenForUser},
expectedCallVerifyAccount: true,
},
{
name: "assorted db error",
token: "abcd1234abcd1234abcd1234abcd1234",
expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError),
storeErrors: TestStoreFunctionsErrors{VerifyAccount: fmt.Errorf("TestStore.VerifyAccount fail")},
expectedCallVerifyAccount: true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
// Set this up to fail according to specification
testStore := TestStore{Errors: tc.storeErrors, TestVerifyTokenString: tc.token}
s := Server{&TestAuth{}, &testStore, &TestEnv{}}
// Make request
req := httptest.NewRequest(http.MethodGet, PathVerify, nil)
q := req.URL.Query()
q.Add("verifyToken", string(testStore.TestVerifyTokenString))
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
s.verify(w, req)
body, _ := ioutil.ReadAll(w.Body)
expectStatusCode(t, w, tc.expectedStatusCode)
expectErrorString(t, body, tc.expectedErrorString)
if tc.expectedCallVerifyAccount != testStore.Called.VerifyAccount {
t.Errorf("Expected Store.VerifyAccount not to be called")
}
})
}
}

View file

@ -22,6 +22,7 @@ const PathPrometheus = "/metrics"
const PathAuthToken = PathPrefix + "/auth/full"
const PathRegister = PathPrefix + "/signup"
const PathVerify = PathPrefix + "/verify"
const PathPassword = PathPrefix + "/password"
const PathWallet = PathPrefix + "/wallet"
const PathClientSaltSeed = PathPrefix + "/client-salt-seed"
@ -152,7 +153,7 @@ func (s *Server) checkAuth(
scope auth.AuthScope,
) *auth.AuthToken {
authToken, err := s.store.GetToken(token)
if err == store.ErrNoToken {
if err == store.ErrNoTokenForUserDevice {
errorJson(w, http.StatusUnauthorized, "Token Not Found")
return nil
}

View file

@ -73,6 +73,7 @@ type TestStoreFunctionsCalled struct {
GetToken auth.TokenString
GetUserId bool
CreateAccount *CreateAccountCall
VerifyAccount bool
SetWallet SetWalletCall
GetWallet bool
ChangePasswordWithWallet ChangePasswordWithWalletCall
@ -85,6 +86,7 @@ type TestStoreFunctionsErrors struct {
GetToken error
GetUserId error
CreateAccount error
VerifyAccount error
SetWallet error
GetWallet error
ChangePasswordWithWallet error
@ -100,7 +102,8 @@ type TestStore struct {
// the test setup
Errors TestStoreFunctionsErrors
TestAuthToken auth.AuthToken
TestAuthToken auth.AuthToken
TestVerifyTokenString auth.VerifyTokenString
TestEncryptedWallet wallet.EncryptedWallet
TestSequence wallet.Sequence
@ -134,6 +137,11 @@ func (s *TestStore) CreateAccount(email auth.Email, password auth.Password, clie
return s.Errors.CreateAccount
}
func (s *TestStore) VerifyAccount(auth.VerifyTokenString) (err error) {
s.Called.VerifyAccount = true
return s.Errors.VerifyAccount
}
func (s *TestStore) SetWallet(
UserId auth.UserId,
encryptedWallet wallet.EncryptedWallet,
@ -259,7 +267,7 @@ func TestServerHelperCheckAuth(t *testing.T) {
expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoToken},
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoTokenForUserDevice},
}, {
name: "unknown auth token db error",
requiredScope: auth.AuthScope("banana"),

View file

@ -47,7 +47,7 @@ func TestServerGetWallet(t *testing.T) {
expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoToken},
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoTokenForUserDevice},
},
{
name: "db error getting wallet",
@ -200,7 +200,7 @@ func TestServerPostWallet(t *testing.T) {
newHmac: wallet.WalletHmac("my-hmac"),
// What causes the error
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoToken},
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoTokenForUserDevice},
}, {
name: "db error setting wallet",
expectedStatusCode: http.StatusInternalServerError,

View file

@ -138,8 +138,8 @@ func TestStoreUpdateToken(t *testing.T) {
expectTokenNotExists(t, &s, authTokenUpdate.Token)
// Try to update the token - fail because we don't have an entry there in the first place
if err := s.updateToken(&authTokenUpdate, expiration); err != ErrNoToken {
t.Fatalf(`updateToken err: wanted "%+v", got "%+v"`, ErrNoToken, err)
if err := s.updateToken(&authTokenUpdate, expiration); err != ErrNoTokenForUserDevice {
t.Fatalf(`updateToken err: wanted "%+v", got "%+v"`, ErrNoTokenForUserDevice, err)
}
// Try to get a token, come back empty because the update attempt failed to do anything
@ -287,8 +287,8 @@ func TestStoreGetToken(t *testing.T) {
// Not found (nothing saved for this token string)
gotToken, err := s.GetToken(authToken.Token)
if gotToken != nil || err != ErrNoToken {
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
if gotToken != nil || err != ErrNoTokenForUserDevice {
t.Fatalf("Expected ErrNoTokenForUserDevice. token: %+v err: %+v", gotToken, err)
}
// Put in a token
@ -317,8 +317,8 @@ func TestStoreGetToken(t *testing.T) {
// Fail to get the expired token
gotToken, err = s.GetToken(authToken.Token)
if gotToken != nil || err != ErrNoToken {
t.Fatalf("Expected ErrNoToken, for expired token. token: %+v err: %+v", gotToken, err)
if gotToken != nil || err != ErrNoTokenForUserDevice {
t.Fatalf("Expected ErrNoTokenForUserDevice, for expired token. token: %+v err: %+v", gotToken, err)
}
}

View file

@ -16,8 +16,9 @@ import (
)
var (
ErrDuplicateToken = fmt.Errorf("Token already exists for this user and device")
ErrNoToken = fmt.Errorf("Token does not exist for this user and device")
ErrDuplicateToken = fmt.Errorf("Token already exists for this user and device")
ErrNoTokenForUserDevice = fmt.Errorf("Token does not exist for this user and device")
ErrNoTokenForUser = fmt.Errorf("Token does not exist for this user")
ErrDuplicateWallet = fmt.Errorf("Wallet already exists for this user")
@ -41,6 +42,7 @@ type StoreInterface interface {
GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error)
GetUserId(auth.Email, auth.Password) (auth.UserId, error)
CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, bool) error
VerifyAccount(auth.VerifyTokenString) error
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) error
GetClientSaltSeed(auth.Email) (auth.ClientSaltSeed, error)
@ -156,7 +158,7 @@ func (s *Store) GetToken(token auth.TokenString) (authToken *auth.AuthToken, err
&authToken.Expiration,
)
if err == sql.ErrNoRows {
err = ErrNoToken
err = ErrNoTokenForUserDevice
}
if err != nil {
authToken = nil
@ -196,7 +198,7 @@ func (s *Store) updateToken(authToken *auth.AuthToken, experation time.Time) (er
return
}
if numRows == 0 {
err = ErrNoToken
err = ErrNoTokenForUserDevice
}
return
}
@ -213,7 +215,7 @@ func (s *Store) SaveToken(token *auth.AuthToken) (err error) {
// device, so there's probably already a token in there.
err = s.updateToken(token, expiration)
if err == ErrNoToken {
if err == ErrNoTokenForUserDevice {
// If we don't have a token already saved, insert a new one:
err = s.insertToken(token, expiration)
@ -381,6 +383,10 @@ func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed aut
return
}
func (s *Store) VerifyAccount(auth.VerifyTokenString) (err error) {
return
}
// Change password. For the user, this requires changing their root password,
// which changes the encryption key for the wallet as well. Thus, we should
// update the wallet at the same time to avoid ever having a situation where