Use verify_token=null as verified to avoid unique constraint

We were using verify_token="" to mean that the user was verified. We need a unique constraint on verify_token to prevent two users from getting the same verify link in their email. This means that if we have two verified users, they will both have verify_token="", which triggers the unique constraint. Oops. However, null is an exception to unique constraints, so we're now using that instead to mean verified.
This commit is contained in:
Daniel Krol 2022-08-13 22:15:19 -04:00
parent e2893c13e3
commit aa691dbc09
9 changed files with 136 additions and 78 deletions

View file

@ -54,7 +54,7 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) {
var registerResponse RegisterResponse var registerResponse RegisterResponse
var token auth.VerifyTokenString var token *auth.VerifyTokenString
modes: modes:
switch verificationMode { switch verificationMode {
@ -78,7 +78,8 @@ modes:
case env.AccountVerificationModeEmailVerify: case env.AccountVerificationModeEmailVerify:
// Not verified until they click their email link. // Not verified until they click their email link.
registerResponse.Verified = false registerResponse.Verified = false
token, err = s.auth.NewVerifyTokenString() newToken, err := s.auth.NewVerifyTokenString()
token = &newToken
if err != nil { if err != nil {
internalServiceErrorJson(w, err, "Error generating verify token string") internalServiceErrorJson(w, err, "Error generating verify token string")
@ -102,8 +103,8 @@ modes:
return return
} }
if len(token) > 0 { if token != nil {
err = s.mail.SendVerificationEmail(registerRequest.Email, token) err = s.mail.SendVerificationEmail(registerRequest.Email, *token)
} }
if err != nil { if err != nil {

View file

@ -243,7 +243,7 @@ func TestServerRegisterAccountVerification(t *testing.T) {
if testStore.Called.CreateAccount == nil { if testStore.Called.CreateAccount == nil {
t.Fatalf("Expected CreateAccount to be called") t.Fatalf("Expected CreateAccount to be called")
} }
tokenPassedIn := testStore.Called.CreateAccount.VerifyToken != "" tokenPassedIn := testStore.Called.CreateAccount.VerifyToken != nil
if tc.expectedVerified && tokenPassedIn { if tc.expectedVerified && tokenPassedIn {
t.Errorf("Expected new account to be verified, thus expected verifyToken *not to be passed in* to call to CreateAccount.") t.Errorf("Expected new account to be verified, thus expected verifyToken *not to be passed in* to call to CreateAccount.")
} }

View file

@ -90,7 +90,7 @@ type CreateAccountCall struct {
Email auth.Email Email auth.Email
Password auth.Password Password auth.Password
ClientSaltSeed auth.ClientSaltSeed ClientSaltSeed auth.ClientSaltSeed
VerifyToken auth.VerifyTokenString VerifyToken *auth.VerifyTokenString
} }
// Whether functions are called, and sometimes what they're called with // Whether functions are called, and sometimes what they're called with
@ -154,7 +154,7 @@ func (s *TestStore) GetUserId(auth.Email, auth.Password) (auth.UserId, error) {
return 0, s.Errors.GetUserId return 0, s.Errors.GetUserId
} }
func (s *TestStore) CreateAccount(email auth.Email, password auth.Password, seed auth.ClientSaltSeed, verifyToken auth.VerifyTokenString) error { func (s *TestStore) CreateAccount(email auth.Email, password auth.Password, seed auth.ClientSaltSeed, verifyToken *auth.VerifyTokenString) error {
s.Called.CreateAccount = &CreateAccountCall{ s.Called.CreateAccount = &CreateAccountCall{
Email: email, Email: email,
Password: password, Password: password,

View file

@ -18,14 +18,14 @@ func expectAccountMatch(
expectedEmail auth.Email, expectedEmail auth.Email,
password auth.Password, password auth.Password,
seed auth.ClientSaltSeed, seed auth.ClientSaltSeed,
expectedVerifyTokenString auth.VerifyTokenString, expectedVerifyTokenString *auth.VerifyTokenString,
approxVerifyExpiration *time.Time, approxVerifyExpiration *time.Time,
) { ) {
var key auth.KDFKey var key auth.KDFKey
var salt auth.ServerSalt var salt auth.ServerSalt
var email auth.Email var email auth.Email
var verifyExpiration *time.Time var verifyExpiration *time.Time
var verifyTokenString auth.VerifyTokenString var verifyTokenString *auth.VerifyTokenString
err := s.db.QueryRow( err := s.db.QueryRow(
`SELECT key, server_salt, email, verify_token, verify_expiration from accounts WHERE normalized_email=? AND client_salt_seed=?`, `SELECT key, server_salt, email, verify_token, verify_expiration from accounts WHERE normalized_email=? AND client_salt_seed=?`,
@ -47,13 +47,23 @@ func expectAccountMatch(
t.Fatalf("Email case not as expected. Want: %s Got: %s", email, expectedEmail) t.Fatalf("Email case not as expected. Want: %s Got: %s", email, expectedEmail)
} }
if verifyTokenString != expectedVerifyTokenString { if (verifyTokenString == nil) != (expectedVerifyTokenString == nil) {
t.Fatalf(
"Verify token string nil-ness not as expected. Want: %v Got: %v",
expectedVerifyTokenString == nil,
verifyTokenString == nil,
)
}
if expectedVerifyTokenString != nil {
if *verifyTokenString != *expectedVerifyTokenString {
t.Fatalf( t.Fatalf(
"Verify token string not as expected. Want: %s Got: %s", "Verify token string not as expected. Want: %s Got: %s",
expectedVerifyTokenString, *expectedVerifyTokenString,
verifyTokenString, *verifyTokenString,
) )
} }
}
if approxVerifyExpiration != nil { if approxVerifyExpiration != nil {
if verifyExpiration == nil { if verifyExpiration == nil {
@ -104,18 +114,18 @@ func TestStoreCreateAccount(t *testing.T) {
// Create an account. Make it verified (i.e. no token) for the usual // Create an account. Make it verified (i.e. no token) for the usual
// case. We'll test unverified (with token) separately. // case. We'll test unverified (with token) separately.
if err := s.CreateAccount(email, password, seed, ""); err != nil { if err := s.CreateAccount(email, password, seed, nil); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err) t.Fatalf("Unexpected error in CreateAccount: %+v", err)
} }
// Get and confirm the account we just put in // Get and confirm the account we just put in
expectAccountMatch(t, &s, normEmail, email, password, seed, "", nil) expectAccountMatch(t, &s, normEmail, email, password, seed, nil, nil)
newPassword := auth.Password("xyz") newPassword := auth.Password("xyz")
// Try to create a new account with the same email and different password, // Try to create a new account with the same email and different password,
// fail because email already exists // fail because email already exists
if err := s.CreateAccount(email, newPassword, seed, ""); err != ErrDuplicateAccount { if err := s.CreateAccount(email, newPassword, seed, nil); err != ErrDuplicateAccount {
t.Fatalf(`CreateAccount err: wanted "%+v", got "%+v"`, ErrDuplicateAccount, err) t.Fatalf(`CreateAccount err: wanted "%+v", got "%+v"`, ErrDuplicateAccount, err)
} }
@ -123,43 +133,79 @@ func TestStoreCreateAccount(t *testing.T) {
// Try to create a new account with the same email different capitalization. // Try to create a new account with the same email different capitalization.
// fail because email already exists // fail because email already exists
if err := s.CreateAccount(differentCaseEmail, password, seed, ""); err != ErrDuplicateAccount { if err := s.CreateAccount(differentCaseEmail, password, seed, nil); err != ErrDuplicateAccount {
t.Fatalf(`CreateAccount err (for case insensitivity check): wanted "%+v", got "%+v"`, ErrDuplicateAccount, err) t.Fatalf(`CreateAccount err (for case insensitivity check): wanted "%+v", got "%+v"`, ErrDuplicateAccount, err)
} }
// Get the email and same *first* password we successfully put in // Get the email and same *first* password we successfully put in
expectAccountMatch(t, &s, normEmail, email, password, seed, "", nil) expectAccountMatch(t, &s, normEmail, email, password, seed, nil, nil)
} }
// Test that I can call CreateAccount twice // Test that I can use CreateAccount twice for different emails with no veriy token
// Try CreateAccount twice with different emails and different password, succeed both times // This is related to https://github.com/lbryio/wallet-sync-server/issues/13
// This is in response to https://github.com/lbryio/wallet-sync-server/issues/13 func TestStoreCreateAccountTwoVerifiedSucceed(t *testing.T) {
func TestStoreCreateTwoAccounts(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
email, normEmail := auth.Email("Abc@Example.Com"), auth.NormalizedEmail("abc@example.com") email1, normEmail1 := auth.Email("Abc@Example.Com"), auth.NormalizedEmail("abc@example.com")
password, seed := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234") password1, seed1 := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
// Create an account. Make it verified (i.e. no token) for the usual email2, normEmail2 := auth.Email("Abc2@Example.Com"), auth.NormalizedEmail("abc2@example.com")
password2, seed2 := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
// Create a couple accounts. Don't care if they have the same password.
// Make them verified (i.e. no token) for the usual
// case. We'll test unverified (with token) separately. // case. We'll test unverified (with token) separately.
if err := s.CreateAccount(email, password, seed, ""); err != nil { if err := s.CreateAccount(email1, password1, seed1, nil); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err) t.Fatalf("Unexpected error in CreateAccount: %+v", err)
} }
email, normEmail = auth.Email("Abc2@Example.Com"), auth.NormalizedEmail("abc2@example.com") if err := s.CreateAccount(email2, password2, seed2, nil); err != nil {
password, seed = auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
// Create another account. Don't care if it has the same password as the first one.
// Make it verified (i.e. no token) for the usual
// case. We'll test unverified (with token) separately.
if err := s.CreateAccount(email, password, seed, ""); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err) t.Fatalf("Unexpected error in CreateAccount: %+v", err)
} }
// Get and confirm the account we just put in // Get and confirm the accounts we just put in
expectAccountMatch(t, &s, normEmail, email, password, seed, "", nil) expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, nil, nil)
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, nil, nil)
}
// Test that I cannot use CreateAccount twice with the same verify token, but
// I can with different verify tokens
// This is related to https://github.com/lbryio/wallet-sync-server/issues/13
func TestStoreCreateAccountTwoSameVerfiyTokenFail(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
email1, normEmail1 := auth.Email("Abc@Example.Com"), auth.NormalizedEmail("abc@example.com")
password1, seed1 := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
verifyToken1 := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
email2, normEmail2 := auth.Email("Abc2@Example.Com"), auth.NormalizedEmail("abc2@example.com")
password2, seed2 := auth.Password("xyz"), auth.ClientSaltSeed("abcd1234abcd1234")
verifyToken2 := auth.VerifyTokenString("00001234abcd1234abcd123400000000")
// Create the first account
if err := s.CreateAccount(email1, password1, seed1, &verifyToken1); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err)
}
// Try to create the second account with the same verify token, fail
if err := s.CreateAccount(email2, password2, seed2, &verifyToken1); err != ErrDuplicateAccount {
t.Fatalf(`CreateAccount err: wanted "%+v", got "%+v"`, ErrDuplicateAccount, err)
}
// Confirm that it didn't save
expectAccountNotExists(t, &s, normEmail2)
// Create the second account with a different verify token
if err := s.CreateAccount(email2, password2, seed2, &verifyToken2); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err)
}
// Get and confirm the accounts we just put in
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, &verifyToken1, &approxVerifyExpiration)
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, &verifyToken2, &approxVerifyExpiration)
} }
// Try CreateAccount with a verification string, thus unverified // Try CreateAccount with a verification string, thus unverified
@ -171,13 +217,14 @@ func TestStoreCreateAccountUnverified(t *testing.T) {
password, seed := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234") password, seed := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
// Create an account // Create an account
if err := s.CreateAccount(email, password, seed, "abcd1234abcd1234abcd1234abcd1234"); err != nil { verifyToken := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
if err := s.CreateAccount(email, password, seed, &verifyToken); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err) t.Fatalf("Unexpected error in CreateAccount: %+v", err)
} }
// Get and confirm the account we just put in // Get and confirm the account we just put in
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC() approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
expectAccountMatch(t, &s, normEmail, email, password, seed, "abcd1234abcd1234abcd1234abcd1234", &approxVerifyExpiration) expectAccountMatch(t, &s, normEmail, email, password, seed, &verifyToken, &approxVerifyExpiration)
} }
// Test GetUserId for nonexisting email // Test GetUserId for nonexisting email
@ -197,7 +244,7 @@ func TestStoreGetUserIdAccountExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
createdUserId, email, password, _ := makeTestUser(t, &s, "", nil) createdUserId, email, password, _ := makeTestUser(t, &s, nil, nil)
// Check that the userId is correct for the email, irrespective of the case of // Check that the userId is correct for the email, irrespective of the case of
// the characters in the email. // the characters in the email.
@ -225,7 +272,8 @@ func TestStoreGetUserIdAccountUnverified(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
_, email, password, _ := makeTestUser(t, &s, "abcd1234abcd1234abcd1234abcd1234", &time.Time{}) verifyToken := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
_, email, password, _ := makeTestUser(t, &s, &verifyToken, &time.Time{})
// Check that it won't return if the account is unverified // Check that it won't return if the account is unverified
if userId, err := s.GetUserId(email, password); err != ErrNotVerified || userId != 0 { if userId, err := s.GetUserId(email, password); err != ErrNotVerified || userId != 0 {
@ -264,7 +312,7 @@ func TestStoreAccountEmptyFields(t *testing.T) {
var sqliteErr sqlite3.Error var sqliteErr sqlite3.Error
err := s.CreateAccount(tc.email, tc.password, tc.clientSaltSeed, "") err := s.CreateAccount(tc.email, tc.password, tc.clientSaltSeed, nil)
if errors.As(err, &sqliteErr) { if errors.As(err, &sqliteErr) {
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintCheck) { if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintCheck) {
return // We got the error we expected return // We got the error we expected
@ -280,7 +328,7 @@ func TestStoreGetClientSaltSeedAccountExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
_, email, _, createdSeed := makeTestUser(t, &s, "", nil) _, email, _, createdSeed := makeTestUser(t, &s, nil, nil)
// Check that the seed is correct for the email, irrespective of the case of // Check that the seed is correct for the email, irrespective of the case of
// the characters in the email. // the characters in the email.
@ -315,7 +363,7 @@ func TestUpdateVerifyTokenStringSuccess(t *testing.T) {
verifyTokenString1 := auth.VerifyTokenString("00000000000000000000000000000000") verifyTokenString1 := auth.VerifyTokenString("00000000000000000000000000000000")
time1 := time.Time{} time1 := time.Time{}
_, email, password, createdSeed := makeTestUser(t, &s, verifyTokenString1, &time1) _, email, password, createdSeed := makeTestUser(t, &s, &verifyTokenString1, &time1)
// we're not testing normalization features so we'll just use this here // we're not testing normalization features so we'll just use this here
normEmail := email.Normalize() normEmail := email.Normalize()
@ -332,12 +380,12 @@ func TestUpdateVerifyTokenStringSuccess(t *testing.T) {
if err := s.UpdateVerifyTokenString(lowerEmail, verifyTokenString2); err != nil { if err := s.UpdateVerifyTokenString(lowerEmail, verifyTokenString2); err != nil {
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err) t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
} }
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, verifyTokenString2, &approxVerifyExpiration) expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString2, &approxVerifyExpiration)
if err := s.UpdateVerifyTokenString(upperEmail, verifyTokenString3); err != nil { if err := s.UpdateVerifyTokenString(upperEmail, verifyTokenString3); err != nil {
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err) t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
} }
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, verifyTokenString3, &approxVerifyExpiration) expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString3, &approxVerifyExpiration)
} }
// Test UpdateVerifyTokenString for nonexisting email // Test UpdateVerifyTokenString for nonexisting email
@ -357,7 +405,7 @@ func TestStoreUpdateVerifyTokenStringAccountVerified(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
_, email, _, _ := makeTestUser(t, &s, "", nil) _, email, _, _ := makeTestUser(t, &s, nil, nil)
if err := s.UpdateVerifyTokenString(email, "abcd1234abcd1234abcd1234abcd1234"); err != ErrNoTokenForUser { if err := s.UpdateVerifyTokenString(email, "abcd1234abcd1234abcd1234abcd1234"); err != ErrNoTokenForUser {
t.Fatalf(`UpdateVerifyTokenString error for already verified account: wanted "%+v", got "%+v."`, ErrNoTokenForUser, err) t.Fatalf(`UpdateVerifyTokenString error for already verified account: wanted "%+v", got "%+v."`, ErrNoTokenForUser, err)
@ -372,7 +420,7 @@ func TestUpdateVerifyAccountSuccess(t *testing.T) {
verifyTokenString := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234") verifyTokenString := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
time1 := time.Time{} time1 := time.Time{}
_, email, password, createdSeed := makeTestUser(t, &s, verifyTokenString, &time1) _, email, password, createdSeed := makeTestUser(t, &s, &verifyTokenString, &time1)
// we're not testing normalization features so we'll just use this here // we're not testing normalization features so we'll just use this here
normEmail := email.Normalize() normEmail := email.Normalize()
@ -380,7 +428,7 @@ func TestUpdateVerifyAccountSuccess(t *testing.T) {
if err := s.VerifyAccount(verifyTokenString); err != nil { if err := s.VerifyAccount(verifyTokenString); err != nil {
t.Fatalf("Unexpected error in VerifyAccount: err: %+v", err) t.Fatalf("Unexpected error in VerifyAccount: err: %+v", err)
} }
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, "", nil) expectAccountMatch(t, &s, normEmail, email, password, createdSeed, nil, nil)
} }
// Test VerifyAccount for nonexisting token // Test VerifyAccount for nonexisting token

View file

@ -77,7 +77,7 @@ func TestStoreInsertToken(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// created for addition to the DB (no expiration attached) // created for addition to the DB (no expiration attached)
authToken1 := auth.AuthToken{ authToken1 := auth.AuthToken{
@ -123,7 +123,7 @@ func TestStoreUpdateToken(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// created for addition to the DB (no expiration attached) // created for addition to the DB (no expiration attached)
authTokenUpdate := auth.AuthToken{ authTokenUpdate := auth.AuthToken{
@ -181,7 +181,7 @@ func TestStoreSaveToken(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// Version 1 of the token for both devices // Version 1 of the token for both devices
// created for addition to the DB (no expiration attached) // created for addition to the DB (no expiration attached)
@ -274,7 +274,7 @@ func TestStoreGetToken(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// created for addition to the DB (no expiration attached) // created for addition to the DB (no expiration attached)
authToken := auth.AuthToken{ authToken := auth.AuthToken{
@ -327,7 +327,7 @@ func TestStoreTokenUTC(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
authToken := auth.AuthToken{ authToken := auth.AuthToken{
Token: "seekrit-1", Token: "seekrit-1",
@ -394,7 +394,7 @@ func TestStoreTokenEmptyFields(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
tc.authToken.UserId, _, _, _ = makeTestUser(t, &s, "", nil) tc.authToken.UserId, _, _, _ = makeTestUser(t, &s, nil, nil)
var sqliteErr sqlite3.Error var sqliteErr sqlite3.Error

View file

@ -16,7 +16,7 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, email, oldPassword, _ := makeTestUser(t, &s, "", nil) userId, email, oldPassword, _ := makeTestUser(t, &s, nil, nil)
token := auth.AuthTokenString("my-token") token := auth.AuthTokenString("my-token")
_, err := s.db.Exec( _, err := s.db.Exec(
@ -47,7 +47,7 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
t.Errorf("ChangePasswordWithWallet (lower case email): unexpected error: %+v", err) t.Errorf("ChangePasswordWithWallet (lower case email): unexpected error: %+v", err)
} }
expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, "", nil) expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, nil, nil)
expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac) expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac)
expectTokenNotExists(t, &s, token) expectTokenNotExists(t, &s, token)
@ -63,17 +63,18 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
t.Errorf("ChangePasswordWithWallet (upper case email): unexpected error: %+v", err) t.Errorf("ChangePasswordWithWallet (upper case email): unexpected error: %+v", err)
} }
expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, "", nil) expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, nil, nil)
} }
func TestStoreChangePasswordErrors(t *testing.T) { func TestStoreChangePasswordErrors(t *testing.T) {
verifyToken := auth.VerifyTokenString("aoeu1234aoeu1234aoeu1234aoeu1234")
tt := []struct { tt := []struct {
name string name string
hasWallet bool hasWallet bool
sequence wallet.Sequence sequence wallet.Sequence
emailSuffix auth.Email emailSuffix auth.Email
oldPasswordSuffix auth.Password oldPasswordSuffix auth.Password
verifyToken auth.VerifyTokenString verifyToken *auth.VerifyTokenString
verifyExpiration *time.Time verifyExpiration *time.Time
expectedError error expectedError error
}{ }{
@ -90,7 +91,7 @@ func TestStoreChangePasswordErrors(t *testing.T) {
sequence: wallet.Sequence(2), // sequence is correct sequence: wallet.Sequence(2), // sequence is correct
emailSuffix: auth.Email(""), // the email is correct emailSuffix: auth.Email(""), // the email is correct
oldPasswordSuffix: auth.Password(""), // the password is correct oldPasswordSuffix: auth.Password(""), // the password is correct
verifyToken: auth.VerifyTokenString("aoeu1234aoeu1234aoeu1234aoeu1234"), verifyToken: &verifyToken,
verifyExpiration: &time.Time{}, verifyExpiration: &time.Time{},
expectedError: ErrNotVerified, expectedError: ErrNotVerified,
}, { }, {
@ -187,7 +188,7 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, email, oldPassword, _ := makeTestUser(t, &s, "", nil) userId, email, oldPassword, _ := makeTestUser(t, &s, nil, nil)
token := auth.AuthTokenString("my-token") token := auth.AuthTokenString("my-token")
_, err := s.db.Exec( _, err := s.db.Exec(
@ -207,7 +208,7 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) {
t.Errorf("ChangePasswordNoWallet (lower case email): unexpected error: %+v", err) t.Errorf("ChangePasswordNoWallet (lower case email): unexpected error: %+v", err)
} }
expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, "", nil) expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, nil, nil)
expectWalletNotExists(t, &s, userId) expectWalletNotExists(t, &s, userId)
expectTokenNotExists(t, &s, token) expectTokenNotExists(t, &s, token)
@ -220,16 +221,18 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) {
t.Errorf("ChangePasswordNoWallet (upper case email): unexpected error: %+v", err) t.Errorf("ChangePasswordNoWallet (upper case email): unexpected error: %+v", err)
} }
expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, "", nil) expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, nil, nil)
} }
func TestStoreChangePasswordNoWalletErrors(t *testing.T) { func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
verifyToken := auth.VerifyTokenString("aoeu1234aoeu1234aoeu1234aoeu1234")
tt := []struct { tt := []struct {
name string name string
hasWallet bool hasWallet bool
emailSuffix auth.Email emailSuffix auth.Email
oldPasswordSuffix auth.Password oldPasswordSuffix auth.Password
verifyToken auth.VerifyTokenString verifyToken *auth.VerifyTokenString
verifyExpiration *time.Time verifyExpiration *time.Time
expectedError error expectedError error
}{ }{
@ -250,7 +253,7 @@ func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
hasWallet: false, // we don't have the wallet, as expected for this function hasWallet: false, // we don't have the wallet, as expected for this function
emailSuffix: auth.Email(""), // the email is correct emailSuffix: auth.Email(""), // the email is correct
oldPasswordSuffix: auth.Password(""), // the password is correct oldPasswordSuffix: auth.Password(""), // the password is correct
verifyToken: auth.VerifyTokenString("aoeu1234aoeu1234aoeu1234aoeu1234"), verifyToken: &verifyToken,
verifyExpiration: &time.Time{}, verifyExpiration: &time.Time{},
expectedError: ErrNotVerified, expectedError: ErrNotVerified,
}, { }, {

View file

@ -46,7 +46,7 @@ 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, auth.ClientSaltSeed, auth.VerifyTokenString) error CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, *auth.VerifyTokenString) error
UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) error UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) error
VerifyAccount(auth.VerifyTokenString) error VerifyAccount(auth.VerifyTokenString) error
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
@ -125,7 +125,13 @@ func (s *Store) Migrate() error {
key TEXT NOT NULL, key TEXT NOT NULL,
client_salt_seed TEXT NOT NULL, client_salt_seed TEXT NOT NULL,
server_salt TEXT NOT NULL, server_salt TEXT NOT NULL,
verify_token TEXT NOT NULL UNIQUE, -- will query by token when verifying
-- UNIQUE because we will query by token when verifying
--
-- Nullable because we want to use null to represent verified users. We can't use empty string
-- because multiple accounts with empty string will trigger the unique constraint, unlike null.
verify_token TEXT UNIQUE,
verify_expiration DATETIME, verify_expiration DATETIME,
user_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER PRIMARY KEY AUTOINCREMENT,
CHECK ( CHECK (
@ -358,7 +364,7 @@ func (s *Store) GetUserId(email auth.Email, password auth.Password) (userId auth
var verified bool var verified bool
err = s.db.QueryRow( err = s.db.QueryRow(
`SELECT user_id, key, server_salt, verify_token="" from accounts WHERE normalized_email=?`, `SELECT user_id, key, server_salt, verify_token is null from accounts WHERE normalized_email=?`,
email.Normalize(), email.Normalize(),
).Scan(&userId, &key, &salt, &verified) ).Scan(&userId, &key, &salt, &verified)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -383,14 +389,14 @@ func (s *Store) GetUserId(email auth.Email, password auth.Password) (userId auth
// Account // // Account //
///////////// /////////////
func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed auth.ClientSaltSeed, verifyToken auth.VerifyTokenString) (err error) { func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed auth.ClientSaltSeed, verifyToken *auth.VerifyTokenString) (err error) {
key, salt, err := password.Create() key, salt, err := password.Create()
if err != nil { if err != nil {
return return
} }
var verifyExpiration *time.Time var verifyExpiration *time.Time
if len(verifyToken) > 0 { if verifyToken != nil {
verifyExpiration = new(time.Time) verifyExpiration = new(time.Time)
*verifyExpiration = time.Now().UTC().Add(VerifyTokenLifespan) *verifyExpiration = time.Now().UTC().Add(VerifyTokenLifespan)
} }
@ -420,7 +426,7 @@ func (s *Store) UpdateVerifyTokenString(email auth.Email, verifyTokenString auth
expiration := time.Now().UTC().Add(VerifyTokenLifespan) expiration := time.Now().UTC().Add(VerifyTokenLifespan)
res, err := s.db.Exec( res, err := s.db.Exec(
`UPDATE accounts SET verify_token=?, verify_expiration=? WHERE normalized_email=? and verify_token!=""`, `UPDATE accounts SET verify_token=?, verify_expiration=? WHERE normalized_email=? and verify_token is not null`,
verifyTokenString, expiration, email.Normalize(), verifyTokenString, expiration, email.Normalize(),
) )
if err != nil { if err != nil {
@ -451,8 +457,8 @@ func (s *Store) UpdateVerifyTokenString(email auth.Email, verifyTokenString auth
func (s *Store) VerifyAccount(verifyTokenString auth.VerifyTokenString) (err error) { func (s *Store) VerifyAccount(verifyTokenString auth.VerifyTokenString) (err error) {
res, err := s.db.Exec( res, err := s.db.Exec(
"UPDATE accounts SET verify_token=?, verify_expiration=? WHERE verify_token=?", "UPDATE accounts SET verify_token=null, verify_expiration=null WHERE verify_token=?",
"", nil, verifyTokenString, verifyTokenString,
) )
if err != nil { if err != nil {
return return
@ -553,7 +559,7 @@ func (s *Store) changePassword(
var verified bool var verified bool
err = tx.QueryRow( err = tx.QueryRow(
`SELECT user_id, key, server_salt, verify_token="" from accounts WHERE normalized_email=?`, `SELECT user_id, key, server_salt, verify_token is null from accounts WHERE normalized_email=?`,
email.Normalize(), email.Normalize(),
).Scan(&userId, &oldKey, &oldSalt, &verified) ).Scan(&userId, &oldKey, &oldSalt, &verified)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {

View file

@ -37,7 +37,7 @@ func StoreTestCleanup(tmpFile *os.File) {
func makeTestUser( func makeTestUser(
t *testing.T, t *testing.T,
s *Store, s *Store,
verifyToken auth.VerifyTokenString, verifyToken *auth.VerifyTokenString,
verifyExpiration *time.Time, verifyExpiration *time.Time,
) (userId auth.UserId, email auth.Email, password auth.Password, seed auth.ClientSaltSeed) { ) (userId auth.UserId, email auth.Email, password auth.Password, seed auth.ClientSaltSeed) {
// email with caps to trigger possible problems // email with caps to trigger possible problems

View file

@ -86,7 +86,7 @@ func TestStoreInsertWallet(t *testing.T) {
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
// Get a valid userId // Get a valid userId
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// Get a wallet, come back empty // Get a wallet, come back empty
expectWalletNotExists(t, &s, userId) expectWalletNotExists(t, &s, userId)
@ -118,7 +118,7 @@ func TestStoreUpdateWallet(t *testing.T) {
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
// Get a valid userId // Get a valid userId
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// Try to update a wallet, fail for nothing to update // Try to update a wallet, fail for nothing to update
if err := s.updateWalletToSequence(userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a")); err != ErrNoWallet { if err := s.updateWalletToSequence(userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a")); err != ErrNoWallet {
@ -174,7 +174,7 @@ func TestStoreSetWallet(t *testing.T) {
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
// Get a valid userId // Get a valid userId
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// Sequence 2 - fails - out of sequence (behind the scenes, tries to update but there's nothing there yet) // Sequence 2 - fails - out of sequence (behind the scenes, tries to update but there's nothing there yet)
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-a")); err != ErrWrongSequence { if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-a")); err != ErrWrongSequence {
@ -221,7 +221,7 @@ func TestStoreGetWallet(t *testing.T) {
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
// Get a valid userId // Get a valid userId
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
// GetWallet fails when there's no wallet // GetWallet fails when there's no wallet
encryptedWallet, sequence, hmac, err := s.GetWallet(userId) encryptedWallet, sequence, hmac, err := s.GetWallet(userId)
@ -264,7 +264,7 @@ func TestStoreWalletEmptyFields(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t) s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, _, _, _ := makeTestUser(t, &s, "", nil) userId, _, _, _ := makeTestUser(t, &s, nil, nil)
var sqliteErr sqlite3.Error var sqliteErr sqlite3.Error