Verify account endpoint
This commit is contained in:
parent
0c6964df0e
commit
f15875c4a6
8 changed files with 169 additions and 16 deletions
|
@ -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("*")
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue