Server test/implement send verify-account email

This commit is contained in:
Daniel Krol 2022-07-27 19:45:09 -04:00
parent f15875c4a6
commit 6672175a25
16 changed files with 249 additions and 125 deletions

View file

@ -19,7 +19,7 @@ type Password string
type KDFKey string // KDF output type KDFKey string // KDF output
type ClientSaltSeed string // part of client-side KDF input along with root password type ClientSaltSeed string // part of client-side KDF input along with root password
type ServerSalt string // server-side KDF input for accounts type ServerSalt string // server-side KDF input for accounts
type TokenString string type AuthTokenString string
type VerifyTokenString string type VerifyTokenString string
type AuthScope string type AuthScope string
@ -28,30 +28,31 @@ const ScopeFull = AuthScope("*")
// For test stubs // For test stubs
type AuthInterface interface { type AuthInterface interface {
// TODO maybe have a "refresh token" thing if the client won't have email available all the time? // TODO maybe have a "refresh token" thing if the client won't have email available all the time?
NewToken(UserId, DeviceId, AuthScope) (*AuthToken, error) NewAuthToken(UserId, DeviceId, AuthScope) (*AuthToken, error)
NewVerifyTokenString() (VerifyTokenString, error)
} }
type Auth struct{} type Auth struct{}
type AuthToken struct { type AuthToken struct {
Token TokenString `json:"token"` Token AuthTokenString `json:"token"`
DeviceId DeviceId `json:"deviceId"` DeviceId DeviceId `json:"deviceId"`
Scope AuthScope `json:"scope"` Scope AuthScope `json:"scope"`
UserId UserId `json:"userId"` UserId UserId `json:"userId"`
Expiration *time.Time `json:"expiration"` Expiration *time.Time `json:"expiration"`
} }
const AuthTokenLength = 32 const TokenLength = 32
func (a *Auth) NewToken(userId UserId, deviceId DeviceId, scope AuthScope) (*AuthToken, error) { func (a *Auth) NewAuthToken(userId UserId, deviceId DeviceId, scope AuthScope) (*AuthToken, error) {
b := make([]byte, AuthTokenLength) b := make([]byte, TokenLength)
// TODO - Is this is a secure random function? (Maybe audit) // TODO - Is this is a secure random function? (Maybe audit)
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {
return nil, fmt.Errorf("Error generating token: %+v", err) return nil, fmt.Errorf("Error generating token: %+v", err)
} }
return &AuthToken{ return &AuthToken{
Token: TokenString(hex.EncodeToString(b)), Token: AuthTokenString(hex.EncodeToString(b)),
DeviceId: deviceId, DeviceId: deviceId,
Scope: scope, Scope: scope,
UserId: userId, UserId: userId,
@ -59,6 +60,16 @@ func (a *Auth) NewToken(userId UserId, deviceId DeviceId, scope AuthScope) (*Aut
}, nil }, nil
} }
func (a *Auth) NewVerifyTokenString() (VerifyTokenString, error) {
b := make([]byte, TokenLength)
// TODO - Is this is a secure random function? (Maybe audit)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("Error generating token: %+v", err)
}
return VerifyTokenString(hex.EncodeToString(b)), nil
}
// NOTE - not stubbing methods of structs like this. more convoluted than it's worth right now // NOTE - not stubbing methods of structs like this. more convoluted than it's worth right now
func (at *AuthToken) ScopeValid(required AuthScope) bool { func (at *AuthToken) ScopeValid(required AuthScope) bool {
// So far * is the only scope issued. Used to have more, didn't want to // So far * is the only scope issued. Used to have more, didn't want to

View file

@ -6,9 +6,9 @@ import (
// Test stubs for now // Test stubs for now
func TestAuthNewToken(t *testing.T) { func TestAuthNewAuthToken(t *testing.T) {
auth := Auth{} auth := Auth{}
authToken, err := auth.NewToken(234, "dId", "my-scope") authToken, err := auth.NewAuthToken(234, "dId", "my-scope")
if err != nil { if err != nil {
t.Fatalf("Error creating new token") t.Fatalf("Error creating new token")
@ -20,8 +20,8 @@ func TestAuthNewToken(t *testing.T) {
t.Fatalf("authToken fields don't match expected values") t.Fatalf("authToken fields don't match expected values")
} }
// result.Token is in hex, AuthTokenLength is bytes in the original // result.Token is in hex, TokenLength is bytes in the original
expectedTokenLength := AuthTokenLength * 2 expectedTokenLength := TokenLength * 2
if len(authToken.Token) != expectedTokenLength { if len(authToken.Token) != expectedTokenLength {
t.Fatalf("authToken token string length isn't the expected length") t.Fatalf("authToken token string length isn't the expected length")
} }

View file

@ -53,6 +53,8 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) {
var registerResponse RegisterResponse var registerResponse RegisterResponse
var token auth.VerifyTokenString
modes: modes:
switch verificationMode { switch verificationMode {
case env.AccountVerificationModeAllowAll: case env.AccountVerificationModeAllowAll:
@ -75,13 +77,19 @@ 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()
if err != nil {
internalServiceErrorJson(w, err, "Error generating verify token string")
return
}
} }
err = s.store.CreateAccount( err = s.store.CreateAccount(
registerRequest.Email, registerRequest.Email,
registerRequest.Password, registerRequest.Password,
registerRequest.ClientSaltSeed, registerRequest.ClientSaltSeed,
registerResponse.Verified, token, // if it's not set, the user is marked as verified
) )
if err != nil { if err != nil {
@ -93,6 +101,15 @@ modes:
return return
} }
if len(token) > 0 {
err = s.mail.SendVerificationEmail(registerRequest.Email, token)
}
if err != nil {
internalServiceErrorJson(w, err, "Error sending verification email")
return
}
response, err := json.Marshal(registerResponse) response, err := json.Marshal(registerResponse)
if err != nil { if err != nil {

View file

@ -7,20 +7,21 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"strings" "strings"
"testing" "testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/store" "lbryio/lbry-id/store"
) )
// TODO - maybe this test could just be one of the TestServerRegisterAccountVerification tests now
func TestServerRegisterSuccess(t *testing.T) { func TestServerRegisterSuccess(t *testing.T) {
testStore := &TestStore{} testStore := &TestStore{}
env := map[string]string{ env := map[string]string{
"ACCOUNT_VERIFICATION_MODE": "AllowAll", "ACCOUNT_VERIFICATION_MODE": "EmailVerify",
} }
s := Server{&TestAuth{}, testStore, &TestEnv{env}} testMail := TestMail{}
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
s := Server{&testAuth, testStore, &TestEnv{env}, &testMail}
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`) requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
@ -35,14 +36,19 @@ func TestServerRegisterSuccess(t *testing.T) {
var result RegisterResponse var result RegisterResponse
err := json.Unmarshal(body, &result) err := json.Unmarshal(body, &result)
expectedResponse := RegisterResponse{Verified: true} expectedResponse := RegisterResponse{Verified: false}
if err != nil || !reflect.DeepEqual(&result, &expectedResponse) { if err != nil || result != expectedResponse {
t.Errorf("Unexpected value for register response. Want: %+v Got: %+v Err: %+v", expectedResponse, result, err) t.Errorf("Unexpected value for register response. Want: %+v Got: %+v Err: %+v", expectedResponse, result, err)
} }
if testStore.Called.CreateAccount == nil { if testStore.Called.CreateAccount == nil {
t.Errorf("Expected Store.CreateAccount to be called") t.Errorf("Expected Store.CreateAccount to be called")
} }
if testMail.SendVerificationEmailCall == nil {
// We're doing EmailVerify for this test.
t.Fatalf("Expected Store.SendVerificationEmail to be called")
}
} }
func TestServerRegisterErrors(t *testing.T) { func TestServerRegisterErrors(t *testing.T) {
@ -51,14 +57,20 @@ func TestServerRegisterErrors(t *testing.T) {
email string email string
expectedStatusCode int expectedStatusCode int
expectedErrorString string expectedErrorString string
expectedCallSendVerificationEmail bool
expectedCallCreateAccount bool
storeErrors TestStoreFunctionsErrors storeErrors TestStoreFunctionsErrors
mailError error
failGenToken bool
}{ }{
{ {
name: "validation error", // missing email address name: "validation error", // missing email address
email: "", email: "",
expectedStatusCode: http.StatusBadRequest, expectedStatusCode: http.StatusBadRequest,
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation: Invalid or missing 'email'", expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation: Invalid or missing 'email'",
expectedCallSendVerificationEmail: false,
expectedCallCreateAccount: false,
// Just check one validation error (missing email address) to make sure the // Just check one validation error (missing email address) to make sure the
// validate function is called. We'll check the rest of the validation // validate function is called. We'll check the rest of the validation
@ -69,6 +81,8 @@ func TestServerRegisterErrors(t *testing.T) {
email: "abc@example.com", email: "abc@example.com",
expectedStatusCode: http.StatusConflict, expectedStatusCode: http.StatusConflict,
expectedErrorString: http.StatusText(http.StatusConflict) + ": Error registering", expectedErrorString: http.StatusText(http.StatusConflict) + ": Error registering",
expectedCallSendVerificationEmail: false,
expectedCallCreateAccount: true,
storeErrors: TestStoreFunctionsErrors{CreateAccount: store.ErrDuplicateEmail}, storeErrors: TestStoreFunctionsErrors{CreateAccount: store.ErrDuplicateEmail},
}, },
@ -77,19 +91,44 @@ func TestServerRegisterErrors(t *testing.T) {
email: "abc@example.com", email: "abc@example.com",
expectedStatusCode: http.StatusInternalServerError, expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError), expectedErrorString: http.StatusText(http.StatusInternalServerError),
expectedCallSendVerificationEmail: false,
expectedCallCreateAccount: true,
storeErrors: TestStoreFunctionsErrors{CreateAccount: fmt.Errorf("TestStore.CreateAccount fail")}, storeErrors: TestStoreFunctionsErrors{CreateAccount: fmt.Errorf("TestStore.CreateAccount fail")},
}, },
{
name: "fail to generate verifiy token",
email: "abc@example.com",
expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError),
expectedCallSendVerificationEmail: false,
expectedCallCreateAccount: false,
failGenToken: true,
},
{
name: "fail to generate verification email",
email: "abc@example.com",
expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError),
expectedCallSendVerificationEmail: true,
expectedCallCreateAccount: true,
mailError: fmt.Errorf("TestEmail.SendVerificationEmail fail"),
},
} }
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
env := map[string]string{ env := map[string]string{
"ACCOUNT_VERIFICATION_MODE": "AllowAll", "ACCOUNT_VERIFICATION_MODE": "EmailVerify",
} }
// Set this up to fail according to specification // Set this up to fail according to specification
s := Server{&TestAuth{}, &TestStore{Errors: tc.storeErrors}, &TestEnv{env}} testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234", FailGenToken: tc.failGenToken}
testMail := TestMail{SendVerificationEmailError: tc.mailError}
testStore := TestStore{Errors: tc.storeErrors}
s := Server{&testAuth, &testStore, &TestEnv{env}, &testMail}
// Make request // Make request
requestBody := fmt.Sprintf(`{"email": "%s", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}`, tc.email) requestBody := fmt.Sprintf(`{"email": "%s", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}`, tc.email)
@ -102,6 +141,20 @@ func TestServerRegisterErrors(t *testing.T) {
expectStatusCode(t, w, tc.expectedStatusCode) expectStatusCode(t, w, tc.expectedStatusCode)
expectErrorString(t, body, tc.expectedErrorString) expectErrorString(t, body, tc.expectedErrorString)
if tc.expectedCallCreateAccount && testStore.Called.CreateAccount == nil {
t.Errorf("Expected Store.CreateAccount to be called")
}
if !tc.expectedCallCreateAccount && testStore.Called.CreateAccount != nil {
t.Errorf("Expected Store.CreateAccount not to be called")
}
if tc.expectedCallSendVerificationEmail && testMail.SendVerificationEmailCall == nil {
t.Errorf("Expected Store.SendVerificationEmail to be called")
}
if !tc.expectedCallSendVerificationEmail && testMail.SendVerificationEmailCall != nil {
t.Errorf("Expected Store.SendVerificationEmail not to be called")
}
}) })
} }
} }
@ -114,6 +167,7 @@ func TestServerRegisterAccountVerification(t *testing.T) {
expectSuccess bool expectSuccess bool
expectedVerified bool expectedVerified bool
expectedStatusCode int expectedStatusCode int
expectedCallSendVerificationEmail bool
}{ }{
{ {
name: "allow all", name: "allow all",
@ -125,6 +179,7 @@ func TestServerRegisterAccountVerification(t *testing.T) {
expectedVerified: true, expectedVerified: true,
expectSuccess: true, expectSuccess: true,
expectedStatusCode: http.StatusCreated, expectedStatusCode: http.StatusCreated,
expectedCallSendVerificationEmail: false,
}, },
{ {
name: "whitelist allowed", name: "whitelist allowed",
@ -137,6 +192,7 @@ func TestServerRegisterAccountVerification(t *testing.T) {
expectedVerified: true, expectedVerified: true,
expectSuccess: true, expectSuccess: true,
expectedStatusCode: http.StatusCreated, expectedStatusCode: http.StatusCreated,
expectedCallSendVerificationEmail: false,
}, },
{ {
name: "whitelist disallowed", name: "whitelist disallowed",
@ -149,6 +205,7 @@ func TestServerRegisterAccountVerification(t *testing.T) {
expectedVerified: false, expectedVerified: false,
expectSuccess: false, expectSuccess: false,
expectedStatusCode: http.StatusForbidden, expectedStatusCode: http.StatusForbidden,
expectedCallSendVerificationEmail: false,
}, },
{ {
name: "email verify", name: "email verify",
@ -160,13 +217,16 @@ func TestServerRegisterAccountVerification(t *testing.T) {
expectedVerified: false, expectedVerified: false,
expectSuccess: true, expectSuccess: true,
expectedStatusCode: http.StatusCreated, expectedStatusCode: http.StatusCreated,
expectedCallSendVerificationEmail: true,
}, },
} }
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
testStore := &TestStore{} testStore := &TestStore{}
s := Server{&TestAuth{}, testStore, &TestEnv{tc.env}} testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
testMail := TestMail{}
s := Server{&testAuth, testStore, &TestEnv{tc.env}, &testMail}
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`) requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
@ -182,8 +242,12 @@ 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")
} }
if tc.expectedVerified != testStore.Called.CreateAccount.Verified { tokenPassedIn := testStore.Called.CreateAccount.VerifyToken != ""
t.Errorf("Unexpected value in call to CreateAccount for `verified`. Want: %+v Got: %+v", tc.expectedVerified, testStore.Called.CreateAccount.Verified) if tc.expectedVerified && tokenPassedIn {
t.Errorf("Expected new account to be verified, thus expected verifyToken *not to be passed in* to call to CreateAccount.")
}
if !tc.expectedVerified && !tokenPassedIn {
t.Errorf("Expected new account not to be verified, thus expected verifyToken not *to be passed in* to call to CreateAccount.")
} }
var result RegisterResponse var result RegisterResponse
err := json.Unmarshal(body, &result) err := json.Unmarshal(body, &result)
@ -197,6 +261,13 @@ func TestServerRegisterAccountVerification(t *testing.T) {
} }
} }
if tc.expectedCallSendVerificationEmail && testMail.SendVerificationEmailCall == nil {
t.Errorf("Expected Store.SendVerificationEmail to be called")
}
if !tc.expectedCallSendVerificationEmail && testMail.SendVerificationEmailCall != nil {
t.Errorf("Expected Store.SendVerificationEmail not to be called")
}
}) })
} }
} }
@ -254,12 +325,12 @@ func TestServerValidateRegisterRequest(t *testing.T) {
} }
func TestServerVerifyAccountSuccess(t *testing.T) { func TestServerVerifyAccountSuccess(t *testing.T) {
testStore := TestStore{TestVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"} testStore := TestStore{}
s := Server{&TestAuth{}, &testStore, &TestEnv{}} s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
req := httptest.NewRequest(http.MethodGet, PathVerify, nil) req := httptest.NewRequest(http.MethodGet, PathVerify, nil)
q := req.URL.Query() q := req.URL.Query()
q.Add("verifyToken", string(testStore.TestVerifyTokenString)) q.Add("verifyToken", "abcd1234abcd1234abcd1234abcd1234")
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -280,7 +351,7 @@ func TestServerVerifyAccountSuccess(t *testing.T) {
func TestServerVerifyAccountErrors(t *testing.T) { func TestServerVerifyAccountErrors(t *testing.T) {
tt := []struct { tt := []struct {
name string name string
token auth.VerifyTokenString token string
expectedStatusCode int expectedStatusCode int
expectedErrorString string expectedErrorString string
expectedCallVerifyAccount bool expectedCallVerifyAccount bool
@ -315,13 +386,13 @@ func TestServerVerifyAccountErrors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// Set this up to fail according to specification // Set this up to fail according to specification
testStore := TestStore{Errors: tc.storeErrors, TestVerifyTokenString: tc.token} testStore := TestStore{Errors: tc.storeErrors}
s := Server{&TestAuth{}, &testStore, &TestEnv{}} s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
// Make request // Make request
req := httptest.NewRequest(http.MethodGet, PathVerify, nil) req := httptest.NewRequest(http.MethodGet, PathVerify, nil)
q := req.URL.Query() q := req.URL.Query()
q.Add("verifyToken", string(testStore.TestVerifyTokenString)) q.Add("verifyToken", tc.token)
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder() w := httptest.NewRecorder()

View file

@ -50,7 +50,7 @@ func (s *Server) getAuthToken(w http.ResponseWriter, req *http.Request) {
return return
} }
authToken, err := s.auth.NewToken(userId, authRequest.DeviceId, auth.ScopeFull) authToken, err := s.auth.NewAuthToken(userId, authRequest.DeviceId, auth.ScopeFull)
if err != nil { if err != nil {
internalServiceErrorJson(w, err, "Error generating auth token") internalServiceErrorJson(w, err, "Error generating auth token")

View file

@ -15,9 +15,9 @@ import (
) )
func TestServerAuthHandlerSuccess(t *testing.T) { func TestServerAuthHandlerSuccess(t *testing.T) {
testAuth := TestAuth{TestNewTokenString: auth.TokenString("seekrit")} testAuth := TestAuth{TestNewAuthTokenString: auth.AuthTokenString("seekrit")}
testStore := TestStore{} testStore := TestStore{}
s := Server{&testAuth, &testStore, &TestEnv{}} s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
requestBody := []byte(`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`) requestBody := []byte(`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`)
@ -32,12 +32,12 @@ func TestServerAuthHandlerSuccess(t *testing.T) {
var result auth.AuthToken var result auth.AuthToken
err := json.Unmarshal(body, &result) err := json.Unmarshal(body, &result)
if err != nil || result.Token != testAuth.TestNewTokenString { if err != nil || result.Token != testAuth.TestNewAuthTokenString {
t.Errorf("Expected auth response to contain token: result: %+v err: %+v", string(body), err) t.Errorf("Expected auth response to contain token: result: %+v err: %+v", string(body), err)
} }
if testStore.Called.SaveToken != testAuth.TestNewTokenString { if testStore.Called.SaveToken != testAuth.TestNewAuthTokenString {
t.Errorf("Expected Store.SaveToken to be called with %s", testAuth.TestNewTokenString) t.Errorf("Expected Store.SaveToken to be called with %s", testAuth.TestNewAuthTokenString)
} }
} }
@ -98,12 +98,12 @@ func TestServerAuthHandlerErrors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// Set this up to fail according to specification // Set this up to fail according to specification
testAuth := TestAuth{TestNewTokenString: auth.TokenString("seekrit")} testAuth := TestAuth{TestNewAuthTokenString: auth.AuthTokenString("seekrit")}
testStore := TestStore{Errors: tc.storeErrors} testStore := TestStore{Errors: tc.storeErrors}
if tc.authFailGenToken { // TODO - TestAuth{Errors:authErrors} if tc.authFailGenToken { // TODO - TestAuth{Errors:authErrors}
testAuth.FailGenToken = true testAuth.FailGenToken = true
} }
server := Server{&testAuth, &testStore, &TestEnv{}} server := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
// Make request // Make request
// So long as the JSON is well-formed, the content doesn't matter here since the password check will be stubbed out // So long as the JSON is well-formed, the content doesn't matter here since the password check will be stubbed out

View file

@ -66,7 +66,7 @@ func TestServerGetClientSalt(t *testing.T) {
Errors: tc.storeErrors, Errors: tc.storeErrors,
} }
s := Server{&testAuth, &testStore, &TestEnv{}} s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
req := httptest.NewRequest(http.MethodGet, PathClientSaltSeed, nil) req := httptest.NewRequest(http.MethodGet, PathClientSaltSeed, nil)
q := req.URL.Query() q := req.URL.Query()

View file

@ -96,7 +96,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
env := map[string]string{ env := map[string]string{
"ACCOUNT_VERIFICATION_MODE": "EmailVerify", "ACCOUNT_VERIFICATION_MODE": "EmailVerify",
} }
s := Server{&auth.Auth{}, &st, &TestEnv{env}} s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}}
//////////////////// ////////////////////
t.Log("Request: Register email address - any device") t.Log("Request: Register email address - any device")
@ -130,8 +130,8 @@ func TestIntegrationWalletUpdates(t *testing.T) {
checkStatusCode(t, statusCode, responseBody) checkStatusCode(t, statusCode, responseBody)
// result.Token is in hex, auth.AuthTokenLength is bytes in the original // result.Token is in hex, auth.TokenLength is bytes in the original
expectedTokenLength := auth.AuthTokenLength * 2 expectedTokenLength := auth.TokenLength * 2
if len(authToken1.Token) != expectedTokenLength { if len(authToken1.Token) != expectedTokenLength {
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody)) t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
} }
@ -265,7 +265,7 @@ func TestIntegrationChangePassword(t *testing.T) {
env := map[string]string{ env := map[string]string{
"ACCOUNT_VERIFICATION_MODE": "EmailVerify", "ACCOUNT_VERIFICATION_MODE": "EmailVerify",
} }
s := Server{&auth.Auth{}, &st, &TestEnv{env}} s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}}
//////////////////// ////////////////////
t.Log("Request: Register email address") t.Log("Request: Register email address")
@ -321,8 +321,8 @@ func TestIntegrationChangePassword(t *testing.T) {
checkStatusCode(t, statusCode, responseBody) checkStatusCode(t, statusCode, responseBody)
// result.Token is in hex, auth.AuthTokenLength is bytes in the original // result.Token is in hex, auth.TokenLength is bytes in the original
expectedTokenLength := auth.AuthTokenLength * 2 expectedTokenLength := auth.TokenLength * 2
if len(authToken.Token) != expectedTokenLength { if len(authToken.Token) != expectedTokenLength {
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody)) t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
} }
@ -404,8 +404,8 @@ func TestIntegrationChangePassword(t *testing.T) {
checkStatusCode(t, statusCode, responseBody) checkStatusCode(t, statusCode, responseBody)
// result.Token is in hex, auth.AuthTokenLength is bytes in the original // result.Token is in hex, auth.TokenLength is bytes in the original
expectedTokenLength = auth.AuthTokenLength * 2 expectedTokenLength = auth.TokenLength * 2
if len(authToken.Token) != expectedTokenLength { if len(authToken.Token) != expectedTokenLength {
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody)) t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
} }
@ -509,8 +509,8 @@ func TestIntegrationChangePassword(t *testing.T) {
checkStatusCode(t, statusCode, responseBody) checkStatusCode(t, statusCode, responseBody)
// result.Token is in hex, auth.AuthTokenLength is bytes in the original // result.Token is in hex, auth.TokenLength is bytes in the original
expectedTokenLength = auth.AuthTokenLength * 2 expectedTokenLength = auth.TokenLength * 2
if len(authToken.Token) != expectedTokenLength { if len(authToken.Token) != expectedTokenLength {
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody)) t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
} }

View file

@ -168,7 +168,7 @@ func TestServerChangePassword(t *testing.T) {
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
testStore := TestStore{Errors: tc.storeErrors} testStore := TestStore{Errors: tc.storeErrors}
s := Server{&TestAuth{}, &testStore, &TestEnv{}} s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
// Whether we passed in wallet fields (these test cases should be passing // 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 // in all of them or none of them, so we only test EncryptedWallet). This

View file

@ -10,6 +10,7 @@ import (
"lbryio/lbry-id/auth" "lbryio/lbry-id/auth"
"lbryio/lbry-id/env" "lbryio/lbry-id/env"
"lbryio/lbry-id/mail"
"lbryio/lbry-id/store" "lbryio/lbry-id/store"
) )
@ -34,6 +35,7 @@ type Server struct {
auth auth.AuthInterface auth auth.AuthInterface
store store.StoreInterface store store.StoreInterface
env env.EnvInterface env env.EnvInterface
mail mail.MailInterface
} }
// TODO If I capitalize the `auth` `store` and `env` fields of Store{} I can // TODO If I capitalize the `auth` `store` and `env` fields of Store{} I can
@ -42,8 +44,9 @@ func Init(
auth auth.AuthInterface, auth auth.AuthInterface,
store store.StoreInterface, store store.StoreInterface,
env env.EnvInterface, env env.EnvInterface,
mail mail.MailInterface,
) *Server { ) *Server {
return &Server{auth, store, env} return &Server{auth, store, env, mail}
} }
type ErrorResponse struct { type ErrorResponse struct {
@ -149,7 +152,7 @@ func getGetData(w http.ResponseWriter, req *http.Request) bool {
// deviceId. Also this is apparently not idiomatic go error handling. // deviceId. Also this is apparently not idiomatic go error handling.
func (s *Server) checkAuth( func (s *Server) checkAuth(
w http.ResponseWriter, w http.ResponseWriter,
token auth.TokenString, token auth.AuthTokenString,
scope auth.AuthScope, scope auth.AuthScope,
) *auth.AuthToken { ) *auth.AuthToken {
authToken, err := s.store.GetToken(token) authToken, err := s.store.GetToken(token)

View file

@ -17,6 +17,21 @@ import (
// Implementing interfaces for stubbed out packages // Implementing interfaces for stubbed out packages
type SendVerificationEmailCall struct {
Email auth.Email
Token auth.VerifyTokenString
}
type TestMail struct {
SendVerificationEmailError error
SendVerificationEmailCall *SendVerificationEmailCall
}
func (m *TestMail) SendVerificationEmail(email auth.Email, token auth.VerifyTokenString) error {
m.SendVerificationEmailCall = &SendVerificationEmailCall{email, token}
return m.SendVerificationEmailError
}
type TestEnv struct { type TestEnv struct {
env map[string]string env map[string]string
} }
@ -26,15 +41,23 @@ func (e *TestEnv) Getenv(key string) string {
} }
type TestAuth struct { type TestAuth struct {
TestNewTokenString auth.TokenString TestNewAuthTokenString auth.AuthTokenString
TestNewVerifyTokenString auth.VerifyTokenString
FailGenToken bool FailGenToken bool
} }
func (a *TestAuth) NewToken(userId auth.UserId, deviceId auth.DeviceId, scope auth.AuthScope) (*auth.AuthToken, error) { func (a *TestAuth) NewAuthToken(userId auth.UserId, deviceId auth.DeviceId, scope auth.AuthScope) (*auth.AuthToken, error) {
if a.FailGenToken { if a.FailGenToken {
return nil, fmt.Errorf("Test error: fail to generate token") return nil, fmt.Errorf("Test error: fail to generate token")
} }
return &auth.AuthToken{Token: a.TestNewTokenString, UserId: userId, DeviceId: deviceId, Scope: scope}, nil return &auth.AuthToken{Token: a.TestNewAuthTokenString, UserId: userId, DeviceId: deviceId, Scope: scope}, nil
}
func (a *TestAuth) NewVerifyTokenString() (auth.VerifyTokenString, error) {
if a.FailGenToken {
return "", fmt.Errorf("Test error: fail to generate token")
}
return a.TestNewVerifyTokenString, nil
} }
type SetWalletCall struct { type SetWalletCall struct {
@ -64,13 +87,13 @@ type CreateAccountCall struct {
Email auth.Email Email auth.Email
Password auth.Password Password auth.Password
ClientSaltSeed auth.ClientSaltSeed ClientSaltSeed auth.ClientSaltSeed
Verified bool 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
type TestStoreFunctionsCalled struct { type TestStoreFunctionsCalled struct {
SaveToken auth.TokenString SaveToken auth.AuthTokenString
GetToken auth.TokenString GetToken auth.AuthTokenString
GetUserId bool GetUserId bool
CreateAccount *CreateAccountCall CreateAccount *CreateAccountCall
VerifyAccount bool VerifyAccount bool
@ -103,7 +126,6 @@ type TestStore struct {
Errors TestStoreFunctionsErrors Errors TestStoreFunctionsErrors
TestAuthToken auth.AuthToken TestAuthToken auth.AuthToken
TestVerifyTokenString auth.VerifyTokenString
TestEncryptedWallet wallet.EncryptedWallet TestEncryptedWallet wallet.EncryptedWallet
TestSequence wallet.Sequence TestSequence wallet.Sequence
@ -117,7 +139,7 @@ func (s *TestStore) SaveToken(authToken *auth.AuthToken) error {
return s.Errors.SaveToken return s.Errors.SaveToken
} }
func (s *TestStore) GetToken(token auth.TokenString) (*auth.AuthToken, error) { func (s *TestStore) GetToken(token auth.AuthTokenString) (*auth.AuthToken, error) {
s.Called.GetToken = token s.Called.GetToken = token
return &s.TestAuthToken, s.Errors.GetToken return &s.TestAuthToken, s.Errors.GetToken
} }
@ -127,12 +149,12 @@ 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, clientSaltSeed auth.ClientSaltSeed, verified bool) 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,
ClientSaltSeed: clientSaltSeed, ClientSaltSeed: seed,
Verified: verified, VerifyToken: verifyToken,
} }
return s.Errors.CreateAccount return s.Errors.CreateAccount
} }
@ -290,9 +312,9 @@ func TestServerHelperCheckAuth(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
testStore := TestStore{ testStore := TestStore{
Errors: tc.storeErrors, Errors: tc.storeErrors,
TestAuthToken: auth.AuthToken{Token: auth.TokenString("seekrit"), Scope: tc.userScope}, TestAuthToken: auth.AuthToken{Token: auth.AuthTokenString("seekrit"), Scope: tc.userScope},
} }
s := Server{&TestAuth{}, &testStore, &TestEnv{}} s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
w := httptest.NewRecorder() w := httptest.NewRecorder()
authToken := s.checkAuth(w, testStore.TestAuthToken.Token, tc.requiredScope) authToken := s.checkAuth(w, testStore.TestAuthToken.Token, tc.requiredScope)

View file

@ -14,7 +14,7 @@ import (
) )
type WalletRequest struct { type WalletRequest struct {
Token auth.TokenString `json:"token"` Token auth.AuthTokenString `json:"token"`
EncryptedWallet wallet.EncryptedWallet `json:"encryptedWallet"` EncryptedWallet wallet.EncryptedWallet `json:"encryptedWallet"`
Sequence wallet.Sequence `json:"sequence"` Sequence wallet.Sequence `json:"sequence"`
Hmac wallet.WalletHmac `json:"hmac"` Hmac wallet.WalletHmac `json:"hmac"`
@ -54,7 +54,7 @@ func (s *Server) handleWallet(w http.ResponseWriter, req *http.Request) {
// TODO - There's probably a struct-based solution here like with POST/PUT. // TODO - There's probably a struct-based solution here like with POST/PUT.
// We could put that struct up top as well. // We could put that struct up top as well.
func getWalletParams(req *http.Request) (token auth.TokenString, err error) { func getWalletParams(req *http.Request) (token auth.AuthTokenString, err error) {
tokenSlice, hasTokenSlice := req.URL.Query()["token"] tokenSlice, hasTokenSlice := req.URL.Query()["token"]
if !hasTokenSlice || tokenSlice[0] == "" { if !hasTokenSlice || tokenSlice[0] == "" {
@ -62,7 +62,7 @@ func getWalletParams(req *http.Request) (token auth.TokenString, err error) {
} }
if err == nil { if err == nil {
token = auth.TokenString(tokenSlice[0]) token = auth.AuthTokenString(tokenSlice[0])
} }
return return

View file

@ -18,7 +18,7 @@ import (
func TestServerGetWallet(t *testing.T) { func TestServerGetWallet(t *testing.T) {
tt := []struct { tt := []struct {
name string name string
tokenString auth.TokenString tokenString auth.AuthTokenString
expectedStatusCode int expectedStatusCode int
expectedErrorString string expectedErrorString string
@ -27,12 +27,12 @@ func TestServerGetWallet(t *testing.T) {
}{ }{
{ {
name: "success", name: "success",
tokenString: auth.TokenString("seekrit"), tokenString: auth.AuthTokenString("seekrit"),
expectedStatusCode: http.StatusOK, expectedStatusCode: http.StatusOK,
}, },
{ {
name: "validation error", // missing auth token name: "validation error", // missing auth token
tokenString: auth.TokenString(""), tokenString: auth.AuthTokenString(""),
expectedStatusCode: http.StatusBadRequest, expectedStatusCode: http.StatusBadRequest,
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Missing token parameter", expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Missing token parameter",
@ -42,7 +42,7 @@ func TestServerGetWallet(t *testing.T) {
}, },
{ {
name: "auth error", name: "auth error",
tokenString: auth.TokenString("seekrit"), tokenString: auth.AuthTokenString("seekrit"),
expectedStatusCode: http.StatusUnauthorized, expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found", expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",
@ -51,7 +51,7 @@ func TestServerGetWallet(t *testing.T) {
}, },
{ {
name: "db error getting wallet", name: "db error getting wallet",
tokenString: auth.TokenString("seekrit"), tokenString: auth.AuthTokenString("seekrit"),
expectedStatusCode: http.StatusInternalServerError, expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError), expectedErrorString: http.StatusText(http.StatusInternalServerError),
@ -65,7 +65,7 @@ func TestServerGetWallet(t *testing.T) {
testAuth := TestAuth{} testAuth := TestAuth{}
testStore := TestStore{ testStore := TestStore{
TestAuthToken: auth.AuthToken{ TestAuthToken: auth.AuthToken{
Token: auth.TokenString(tc.tokenString), Token: auth.AuthTokenString(tc.tokenString),
Scope: auth.ScopeFull, Scope: auth.ScopeFull,
}, },
@ -77,7 +77,7 @@ func TestServerGetWallet(t *testing.T) {
} }
testEnv := TestEnv{} testEnv := TestEnv{}
s := Server{&testAuth, &testStore, &testEnv} s := Server{&testAuth, &testStore, &testEnv, &TestMail{}}
req := httptest.NewRequest(http.MethodGet, PathWallet, nil) req := httptest.NewRequest(http.MethodGet, PathWallet, nil)
q := req.URL.Query() q := req.URL.Query()
@ -228,14 +228,14 @@ func TestServerPostWallet(t *testing.T) {
testAuth := TestAuth{} testAuth := TestAuth{}
testStore := TestStore{ testStore := TestStore{
TestAuthToken: auth.AuthToken{ TestAuthToken: auth.AuthToken{
Token: auth.TokenString("seekrit"), Token: auth.AuthTokenString("seekrit"),
Scope: auth.ScopeFull, Scope: auth.ScopeFull,
}, },
Errors: tc.storeErrors, Errors: tc.storeErrors,
} }
s := Server{&testAuth, &testStore, &TestEnv{}} s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
requestBody := []byte( requestBody := []byte(
fmt.Sprintf(`{ fmt.Sprintf(`{

View file

@ -43,7 +43,7 @@ func expectTokenExists(t *testing.T, s *Store, expectedToken auth.AuthToken) {
t.Fatalf("Expected token for: %s", expectedToken.Token) t.Fatalf("Expected token for: %s", expectedToken.Token)
} }
func expectTokenNotExists(t *testing.T, s *Store, token auth.TokenString) { func expectTokenNotExists(t *testing.T, s *Store, token auth.AuthTokenString) {
rows, err := s.db.Query("SELECT * FROM auth_tokens WHERE token=?", token) rows, err := s.db.Query("SELECT * FROM auth_tokens WHERE token=?", token)
if err != nil { if err != nil {
t.Fatalf("Error finding (lack of) token for: %s - %+v", token, err) t.Fatalf("Error finding (lack of) token for: %s - %+v", token, err)

View file

@ -17,7 +17,7 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, email, oldPassword, _ := makeTestUser(t, &s) userId, email, oldPassword, _ := makeTestUser(t, &s)
token := auth.TokenString("my-token") token := auth.AuthTokenString("my-token")
_, err := s.db.Exec( _, err := s.db.Exec(
"INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) VALUES(?,?,?,?,?)", "INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) VALUES(?,?,?,?,?)",
@ -117,7 +117,7 @@ func TestStoreChangePasswordErrors(t *testing.T) {
userId, email, oldPassword, oldSeed := makeTestUser(t, &s) userId, email, oldPassword, oldSeed := makeTestUser(t, &s)
expiration := time.Now().UTC().Add(time.Hour * 24 * 14) expiration := time.Now().UTC().Add(time.Hour * 24 * 14)
authToken := auth.AuthToken{ authToken := auth.AuthToken{
Token: auth.TokenString("my-token"), Token: auth.AuthTokenString("my-token"),
DeviceId: auth.DeviceId("my-dev-id"), DeviceId: auth.DeviceId("my-dev-id"),
UserId: userId, UserId: userId,
Scope: auth.AuthScope("*"), Scope: auth.AuthScope("*"),
@ -177,7 +177,7 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) {
defer StoreTestCleanup(sqliteTmpFile) defer StoreTestCleanup(sqliteTmpFile)
userId, email, oldPassword, _ := makeTestUser(t, &s) userId, email, oldPassword, _ := makeTestUser(t, &s)
token := auth.TokenString("my-token") token := auth.AuthTokenString("my-token")
_, err := s.db.Exec( _, err := s.db.Exec(
"INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) VALUES(?,?,?,?,?)", "INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) VALUES(?,?,?,?,?)",
@ -249,7 +249,7 @@ func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
userId, email, oldPassword, oldSeed := makeTestUser(t, &s) userId, email, oldPassword, oldSeed := makeTestUser(t, &s)
expiration := time.Now().UTC().Add(time.Hour * 24 * 14) expiration := time.Now().UTC().Add(time.Hour * 24 * 14)
authToken := auth.AuthToken{ authToken := auth.AuthToken{
Token: auth.TokenString("my-token"), Token: auth.AuthTokenString("my-token"),
DeviceId: auth.DeviceId("my-dev-id"), DeviceId: auth.DeviceId("my-dev-id"),
UserId: userId, UserId: userId,
Scope: auth.AuthScope("*"), Scope: auth.AuthScope("*"),

View file

@ -37,11 +37,11 @@ var (
// For test stubs // For test stubs
type StoreInterface interface { type StoreInterface interface {
SaveToken(*auth.AuthToken) error SaveToken(*auth.AuthToken) error
GetToken(auth.TokenString) (*auth.AuthToken, error) GetToken(auth.AuthTokenString) (*auth.AuthToken, error)
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, bool) error CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, 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
ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) error ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) error
@ -143,7 +143,7 @@ func (s *Store) Migrate() error {
// (which I did previously)? // (which I did previously)?
// //
// TODO Put the timestamp in the token to avoid duplicates over time. And/or just use a library! Someone solved this already. // TODO Put the timestamp in the token to avoid duplicates over time. And/or just use a library! Someone solved this already.
func (s *Store) GetToken(token auth.TokenString) (authToken *auth.AuthToken, err error) { func (s *Store) GetToken(token auth.AuthTokenString) (authToken *auth.AuthToken, err error) {
expirationCutoff := time.Now().UTC() expirationCutoff := time.Now().UTC()
authToken = &(auth.AuthToken{}) authToken = &(auth.AuthToken{})
@ -362,7 +362,7 @@ 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, verified bool) (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