diff --git a/authStore.go b/authStore.go index b3ef62e..d47bdce 100644 --- a/authStore.go +++ b/authStore.go @@ -1,10 +1,6 @@ package main import ( - "crypto/rand" - "crypto/sha256" - "crypto/subtle" - "encoding/base64" "encoding/json" "io" "io/ioutil" @@ -35,6 +31,7 @@ type emailCookie struct { type authStore struct { backend BackendQuerier sessionStore SessionStorer + loginStore LoginStorer mailer Mailer cookieStore CookieStorer r *http.Request @@ -44,7 +41,8 @@ var emailRegex = regexp.MustCompile(`^(?i)[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$ func NewAuthStore(backend BackendQuerier, mailer Mailer, w http.ResponseWriter, r *http.Request, cookieKey []byte, cookiePrefix string, secureOnlyCookie bool) AuthStorer { sessionStore := NewSessionStore(backend, w, r, cookieKey, cookiePrefix, secureOnlyCookie) - return &authStore{backend, sessionStore, mailer, NewCookieStore(w, r, cookieKey, secureOnlyCookie), r} + loginStore := NewLoginStore(backend, mailer, r) + return &authStore{backend, sessionStore, loginStore, mailer, NewCookieStore(w, r, cookieKey, secureOnlyCookie), r} } func (s *authStore) GetSession() (*UserLoginSession, error) { @@ -54,14 +52,11 @@ func (s *authStore) GetSession() (*UserLoginSession, error) { func (s *authStore) GetBasicAuth() (*UserLoginSession, error) { session, err := s.GetSession() if err != nil { - if email, password, ok := s.r.BasicAuth(); ok { - session, err = s.login(email, password, false) - if err != nil { - return nil, newLoggedError("Unable to login with provided credentials", err) - } - } else { - return nil, newAuthError("Problem decoding credentials from basic auth", nil) + login, err := s.loginStore.LoginBasic() + if err != nil { + return nil, err } + return s.sessionStore.CreateSession(login.LoginID, login.UserID, false) } return session, nil } @@ -76,23 +71,10 @@ func (s *authStore) Login() error { } func (s *authStore) login(email, password string, rememberMe bool) (*UserLoginSession, error) { - if !isValidEmail(email) { - return nil, newAuthError("Please enter a valid email address.", nil) - } - if !isValidPassword(password) { - return nil, newAuthError(passwordValidationMessage, nil) - } - - login, err := s.backend.GetLogin(email, loginProviderDefaultName) + login, err := s.loginStore.Login(email, password, rememberMe) if err != nil { - return nil, newLoggedError("Invalid username or password", err) - } - - decoded, _ := decodeFromString(login.ProviderKey) - if !hashEquals([]byte(password), decoded) { - return nil, newLoggedError("Invalid username or password", nil) + return nil, err } - return s.sessionStore.CreateSession(login.LoginID, login.UserID, rememberMe) } @@ -172,10 +154,14 @@ func (s *authStore) createProfile(fullName, organization, password, picturePath return newLoggedError("Invalid email verification cookie", err) } - passwordHash := encodeToString(hash([]byte(password))) - login, err := s.backend.CreateLogin(emailVerifyHash, passwordHash, fullName, organization, picturePath) + email, err := s.backend.UpdateUser(emailVerifyHash, fullName, organization, picturePath) + if err != nil { + return newLoggedError("Unable to update user", err) + } + + login, err := s.loginStore.CreateLogin(email, fullName, password) if err != nil { - return newLoggedError("Unable to create profile", err) + return newLoggedError("Unable to create login", err) } _, err = s.sessionStore.CreateSession(login.LoginID, login.UserID, false) @@ -333,77 +319,6 @@ func getJSON(r *http.Request, result interface{}) error { const passwordValidationMessage string = "Password must be between 7 and 20 characters" -func isValidPassword(password string) bool { - return len(password) >= 7 && len(password) <= 20 -} - func isValidEmail(email string) bool { return len(email) <= 254 && len(email) >= 6 && emailRegex.MatchString(email) == true } - -func decodeStringToHash(token string) (string, error) { - data, err := decodeFromString(token) - if err != nil { - return "", err - } - return encodeToString(hash(data)), nil -} - -func decodeFromString(token string) ([]byte, error) { - return base64.URLEncoding.DecodeString(token) -} - -func encodeToString(bytes []byte) string { - return base64.URLEncoding.EncodeToString(bytes) -} - -func generateSelectorTokenAndHash() (string, string, string, error) { - var selector, token, tokenHash string - selector, err := generateRandomString() - if err != nil { - return "", "", "", newLoggedError("Unable to generate rememberMe selector", err) - } - token, tokenHash, err = generateStringAndHash() - if err != nil { - return "", "", "", newLoggedError("Unable to generate rememberMe token", err) - } - return selector, token, tokenHash, nil -} - -func generateStringAndHash() (string, string, error) { - b, err := generateRandomBytes(32) - if err != nil { - return "", "", err - } - return encodeToString(b), encodeToString(hash(b)), nil -} - -func hash(bytes []byte) []byte { - h := sha256.Sum256(bytes) - return h[:] -} - -// Url decode both the token and the hash and then compare -func encodedHashEquals(token, tokenHash string) bool { - tokenBytes, _ := decodeFromString(token) - hashBytes, _ := decodeFromString(tokenHash) - return hashEquals(tokenBytes, hashBytes) -} - -func hashEquals(token, tokenHash []byte) bool { - return subtle.ConstantTimeCompare(hash(token), tokenHash) == 1 -} - -func generateRandomString() (string, error) { - bytes, err := generateRandomBytes(32) - return encodeToString(bytes), err -} - -func generateRandomBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - if err != nil { - return nil, err - } - return b, nil -} diff --git a/authStore_test.go b/authStore_test.go index f644cd2..704cd51 100644 --- a/authStore_test.go +++ b/authStore_test.go @@ -17,11 +17,12 @@ import ( var futureTime = time.Now().Add(5 * time.Minute) var pastTime = time.Now().Add(-5 * time.Minute) -func getAuthStore(createSessionReturn *SessionReturn, emailCookieToReturn *emailCookie, hasCookieGetError, hasCookiePutError bool, mailErr error, backend *MockBackend) *authStore { +func getAuthStore(createSessionReturn *SessionReturn, loginReturn *LoginReturn, emailCookieToReturn *emailCookie, hasCookieGetError, hasCookiePutError bool, mailErr error, backend *MockBackend) *authStore { r := &http.Request{} cookieStore := NewMockCookieStore(map[string]interface{}{emailCookieName: emailCookieToReturn}, hasCookieGetError, hasCookiePutError) sessionStore := MockSessionStore{CreateSessionReturn: createSessionReturn} - return &authStore{backend, &sessionStore, &TextMailer{Err: mailErr}, cookieStore, r} + loginStore := MockLoginStore{LoginReturn: loginReturn} + return &authStore{backend, &sessionStore, &loginStore, &TextMailer{Err: mailErr}, cookieStore, r} } func TestNewAuthStore(t *testing.T) { @@ -69,6 +70,10 @@ func TestAuthStoreEndToEnd(t *testing.T) { // create profile err = s.createProfile("fullName", "company", "password", "picturePath") + expectedPassword := encodeToString(hash([]byte("password"))) + if err != nil || len(b.Users) != 1 || len(b.Sessions) != 1 || len(b.Logins) != 1 || b.Logins[0].LoginID != 1 || b.Logins[0].UserID != 1 || b.Logins[0].ProviderKey != expectedPassword { + t.Fatal("expected valid user, login and session", b.Logins[0], expectedPassword, b.Logins[0].ProviderKey) + } // decode session cookie value = substring.Between(w.HeaderMap["Set-Cookie"][1], "prefixSession=", ";") @@ -87,76 +92,14 @@ func TestAuthStoreEndToEnd(t *testing.T) { // login on same browser with same existing session session, err := s.login("test@test.com", "password", true) if err != nil || len(b.Logins) != 1 || len(b.Sessions) != 1 || len(b.Users) != 1 || session.SessionHash != b.Sessions[0].SessionHash || session.UserID != 1 { - t.Fatal("expected to login to existing session", len(b.Logins), len(b.Sessions), len(b.Users), session, b.Sessions[0].SessionHash) + t.Fatal("expected to login to existing session", err, len(b.Logins), len(b.Sessions), len(b.Users), session, b.Sessions[0].SessionHash) } // now login with different browser with new session ID. Create new session - /* session, rememberMe, err = b.NewLoginSession(login.LoginId, "newSessionHash", time.Now().UTC().AddDate(0, 0, 1), time.Now().UTC().AddDate(0, 0, 5), false, "", "", time.Time{}, time.Time{}) - if err != nil || login == nil || rememberMe != nil || len(b.Sessions) != 2 { - t.Fatal("expected new User Login to be created") - }*/ -} - -var loginTests = []struct { - Scenario string - Email string - Password string - RememberMe bool - CreateSessionReturn *SessionReturn - GetUserLoginReturn *LoginReturn - ErrReturn error - MethodsCalled []string - ExpectedResult *UserLoginRememberMe - ExpectedErr string -}{ - { - Scenario: "Invalid email", - Email: "invalid@bogus", - ExpectedErr: "Please enter a valid email address.", - }, - { - Scenario: "Invalid password", - Email: "email@example.com", - Password: "short", - ExpectedErr: passwordValidationMessage, - }, - { - Scenario: "Can't get login", - Email: "email@example.com", - Password: "validPassword", - GetUserLoginReturn: loginErr(), - MethodsCalled: []string{"GetUserLogin"}, - ExpectedErr: "Invalid username or password", - }, - { - Scenario: "Incorrect password", - Email: "email@example.com", - Password: "wrongPassword", - GetUserLoginReturn: &LoginReturn{Login: &UserLogin{LoginID: 1, UserID: 1, ProviderKey: "1234"}}, - MethodsCalled: []string{"GetUserLogin"}, - ExpectedErr: "Invalid username or password", - }, - { - Scenario: "Got session", - Email: "email@example.com", - Password: "correctPassword", - GetUserLoginReturn: loginSuccess(), - CreateSessionReturn: sessionSuccess(futureTime, futureTime), - MethodsCalled: []string{"GetUserLogin"}, - }, -} - -func TestAuthLogin(t *testing.T) { - for i, test := range loginTests { - backend := &MockBackend{GetUserLoginReturn: test.GetUserLoginReturn, ErrReturn: test.ErrReturn} - store := getAuthStore(test.CreateSessionReturn, nil, true, false, nil, backend) // cookie get error so we don't try to invalidate old session or rememberme - val, err := store.login(test.Email, test.Password, test.RememberMe) - methods := store.backend.(*MockBackend).MethodsCalled - if (err == nil && test.ExpectedErr != "" || err != nil && test.ExpectedErr != err.Error()) || - !collectionEqual(test.MethodsCalled, methods) { - t.Errorf("Scenario[%d] failed: %s\nexpected err:%v\tactual err:%v\nexpected val:%v\tactual val:%v\nexpected methods: %s\tactual methods: %s", i, test.Scenario, test.ExpectedErr, err, test.ExpectedResult, val, test.MethodsCalled, methods) - } - } + //session, rememberMe, err = b.NewLoginSession(login.LoginId, "newSessionHash", time.Now().UTC().AddDate(0, 0, 1), time.Now().UTC().AddDate(0, 0, 5), false, "", "", time.Time{}, time.Time{}) + //if err != nil || login == nil || rememberMe != nil || len(b.Sessions) != 2 { + // t.Fatal("expected new User Login to be created") + //} } var registerTests = []struct { @@ -188,7 +131,7 @@ var registerTests = []struct { func TestAuthRegister(t *testing.T) { for i, test := range registerTests { backend := &MockBackend{AddUserReturn: test.AddUserReturn} - store := getAuthStore(nil, nil, false, false, nil, backend) + store := getAuthStore(nil, nil, nil, false, false, nil, backend) err := store.register(test.Email) methods := store.backend.(*MockBackend).MethodsCalled if (err == nil && test.ExpectedErr != "" || err != nil && test.ExpectedErr != err.Error()) || @@ -203,7 +146,8 @@ var createProfileTests = []struct { HasCookieGetError bool HasCookiePutError bool EmailCookie *emailCookie - CreateLoginReturn *LoginReturn + LoginReturn *LoginReturn + UpdateUserReturn error CreateSessionReturn *SessionReturn MethodsCalled []string ExpectedErr string @@ -219,34 +163,41 @@ var createProfileTests = []struct { ExpectedErr: "Invalid email verification cookie", }, { - Scenario: "Error Creating profile", - EmailCookie: &emailCookie{EmailVerificationCode: "nfwRDzfxxJj2_HY-_mLz6jWyWU7bF0zUlIUUVkQgbZ0=", ExpireTimeUTC: time.Now()}, - CreateLoginReturn: loginErr(), - MethodsCalled: []string{"CreateLogin"}, - ExpectedErr: "Unable to create profile", + Scenario: "Error Updating user", + EmailCookie: &emailCookie{EmailVerificationCode: "nfwRDzfxxJj2_HY-_mLz6jWyWU7bF0zUlIUUVkQgbZ0=", ExpireTimeUTC: time.Now()}, + UpdateUserReturn: errors.New("failed"), + LoginReturn: loginErr(), + MethodsCalled: []string{"UpdateUser"}, + ExpectedErr: "Unable to update user", + }, + { + Scenario: "Error Creating login", + EmailCookie: &emailCookie{EmailVerificationCode: "nfwRDzfxxJj2_HY-_mLz6jWyWU7bF0zUlIUUVkQgbZ0=", ExpireTimeUTC: time.Now()}, + LoginReturn: loginErr(), + MethodsCalled: []string{"UpdateUser"}, + ExpectedErr: "Unable to create login", }, { - Scenario: "Error getting session", + Scenario: "Error creating session", EmailCookie: &emailCookie{EmailVerificationCode: "nfwRDzfxxJj2_HY-_mLz6jWyWU7bF0zUlIUUVkQgbZ0=", ExpireTimeUTC: time.Now()}, - HasCookiePutError: true, - CreateLoginReturn: loginSuccess(), + LoginReturn: loginSuccess(), CreateSessionReturn: sessionErr(), - MethodsCalled: []string{"CreateLogin"}, + MethodsCalled: []string{"UpdateUser"}, ExpectedErr: "failed", }, { Scenario: "Success", EmailCookie: &emailCookie{EmailVerificationCode: "nfwRDzfxxJj2_HY-_mLz6jWyWU7bF0zUlIUUVkQgbZ0=", ExpireTimeUTC: time.Now()}, - CreateLoginReturn: loginSuccess(), + LoginReturn: loginSuccess(), CreateSessionReturn: sessionSuccess(futureTime, futureTime), - MethodsCalled: []string{"CreateLogin"}, + MethodsCalled: []string{"UpdateUser"}, }, } func TestAuthCreateProfile(t *testing.T) { for i, test := range createProfileTests { - backend := &MockBackend{CreateLoginReturn: test.CreateLoginReturn} - store := getAuthStore(test.CreateSessionReturn, test.EmailCookie, test.HasCookieGetError, test.HasCookiePutError, nil, backend) + backend := &MockBackend{ErrReturn: test.UpdateUserReturn} + store := getAuthStore(test.CreateSessionReturn, test.LoginReturn, test.EmailCookie, test.HasCookieGetError, test.HasCookiePutError, nil, backend) err := store.createProfile("name", "organization", "password", "path") methods := store.backend.(*MockBackend).MethodsCalled if (err == nil && test.ExpectedErr != "" || err != nil && test.ExpectedErr != err.Error()) || @@ -305,7 +256,7 @@ var verifyEmailTests = []struct { func TestAuthVerifyEmail(t *testing.T) { for i, test := range verifyEmailTests { backend := &MockBackend{VerifyEmailReturn: test.VerifyEmailReturn} - store := getAuthStore(nil, nil, false, test.HasCookiePutError, test.MailErr, backend) + store := getAuthStore(nil, nil, nil, false, test.HasCookiePutError, test.MailErr, backend) err := store.verifyEmail(test.EmailVerificationCode) methods := store.backend.(*MockBackend).MethodsCalled if (err == nil && test.ExpectedErr != "" || err != nil && test.ExpectedErr != err.Error()) || @@ -330,7 +281,7 @@ func TestRegisterPub(t *testing.T) { buf.WriteString(`{"Email":"bogus"}`) r := &http.Request{Body: ioutil.NopCloser(&buf)} backend := &MockBackend{} - store := getAuthStore(nil, nil, true, false, nil, backend) + store := getAuthStore(nil, nil, nil, true, false, nil, backend) store.r = r err := store.Register() if err == nil || err.Error() != "Invalid email" { @@ -359,7 +310,7 @@ func TestVerifyEmailPub(t *testing.T) { buf.WriteString(`{"EmailVerificationCode":"nfwRDzfxxJj2_HY-_mLz6jWyWU7bF0zUlIUUVkQgbZ0"}`) // random valid base64 encoded data r := &http.Request{Body: ioutil.NopCloser(&buf)} backend := &MockBackend{VerifyEmailReturn: verifyEmailErr()} - store := getAuthStore(nil, nil, true, false, nil, backend) + store := getAuthStore(nil, nil, nil, true, false, nil, backend) store.r = r err := store.VerifyEmail() if err == nil || err.Error() != "Failed to verify email" { @@ -388,10 +339,10 @@ func TestLoginJson(t *testing.T) { buf.WriteString(`{"Email":"email", "Password":"password", "RememberMe":true}`) r := &http.Request{Body: ioutil.NopCloser(&buf)} backend := &MockBackend{} - store := getAuthStore(nil, nil, true, false, nil, backend) + store := getAuthStore(nil, loginErr(), nil, true, false, nil, backend) store.r = r err := store.Login() - if err == nil || err.Error() != "Please enter a valid email address." { + if err == nil || err.Error() != "failed" { t.Error("expected error from login method", err) } @@ -437,7 +388,7 @@ func TestCreateProfilePub(t *testing.T) { r, _ := http.NewRequest("PUT", "url", &buf) r.Header.Add("Content-Type", w.FormDataContentType()) backend := &MockBackend{} - store := getAuthStore(nil, nil, true, false, nil, backend) + store := getAuthStore(nil, nil, nil, true, false, nil, backend) store.r = r err := store.CreateProfile() if err == nil || err.Error() != "Unable to get email verification cookie" { diff --git a/backend.go b/backend.go index dbc4a5f..8935fd3 100644 --- a/backend.go +++ b/backend.go @@ -8,11 +8,11 @@ import ( type BackendQuerier interface { AddUser(email, emailVerifyHash string) error VerifyEmail(emailVerifyHash string) (string, error) - UpdateUser(session *UserLoginSession, fullname string, company string, pictureURL string) error + UpdateUser(emailVerifyHash, fullname string, company string, pictureURL string) (string, error) UpdateEmailAndInvalidateSessions(email string, password string, newEmail string) (*UserLoginSession, error) UpdatePasswordAndInvalidateSessions(email string, oldPassword string, newPassword string) (*UserLoginSession, error) - CreateLogin(emailVerifyHash, passwordHash string, fullName string, company string, pictureURL string) (*UserLogin, error) + CreateLogin(email, passwordHash, fullName string) (*UserLogin, error) GetLogin(email, loginProvider string) (*UserLogin, error) CreateSession(loginID, userID int, sessionHash string, sessionRenewTimeUTC, sessionExpireTimeUTC time.Time, rememberMe bool, rememberMeSelector, rememberMeTokenHash string, rememberMeRenewTimeUTC, rememberMeExpireTimeUTC time.Time) (*UserLoginSession, *UserLoginRememberMe, error) diff --git a/backendMemory.go b/backendMemory.go index 517f6f2..60702d2 100644 --- a/backendMemory.go +++ b/backendMemory.go @@ -66,6 +66,7 @@ func (m *backendMemory) CreateSession(loginID, userID int, sessionHash string, s } return session, rememberItem, nil } + func (m *backendMemory) GetSession(sessionHash string) (*UserLoginSession, error) { session := m.getSessionByHash(sessionHash) if session == nil { @@ -73,6 +74,7 @@ func (m *backendMemory) GetSession(sessionHash string) (*UserLoginSession, error } return session, nil } + func (m *backendMemory) RenewSession(sessionHash string, renewTimeUTC time.Time) (*UserLoginSession, error) { session := m.getSessionByHash(sessionHash) if session == nil { @@ -81,6 +83,7 @@ func (m *backendMemory) RenewSession(sessionHash string, renewTimeUTC time.Time) session.RenewTimeUTC = renewTimeUTC return session, nil } + func (m *backendMemory) GetRememberMe(selector string) (*UserLoginRememberMe, error) { rememberMe := m.getRememberMe(selector) if rememberMe == nil { @@ -88,6 +91,7 @@ func (m *backendMemory) GetRememberMe(selector string) (*UserLoginRememberMe, er } return rememberMe, nil } + func (m *backendMemory) RenewRememberMe(selector string, renewTimeUTC time.Time) (*UserLoginRememberMe, error) { rememberMe := m.getRememberMe(selector) if rememberMe == nil { @@ -100,6 +104,7 @@ func (m *backendMemory) RenewRememberMe(selector string, renewTimeUTC time.Time) rememberMe.RenewTimeUTC = renewTimeUTC return rememberMe, nil } + func (m *backendMemory) AddUser(email, emailVerifyHash string) error { if m.getUserByEmail(email) != nil { return errUserAlreadyExists @@ -124,18 +129,23 @@ func (m *backendMemory) VerifyEmail(emailVerifyHash string) (string, error) { return user.PrimaryEmail, nil } -func (m *backendMemory) UpdateUser(session *UserLoginSession, fullname string, company string, pictureURL string) error { - return nil +func (m *backendMemory) UpdateUser(emailVerifyHash, fullname string, company string, pictureURL string) (string, error) { + user := m.getUserByEmailVerifyHash(emailVerifyHash) + if user == nil { + return "", errUserNotFound + } + user.FullName = fullname + // need to be able to create company and set pictureURL + return user.PrimaryEmail, nil } -// This function isn't right yet. Not creating company. Not sure if anything else is missing -func (m *backendMemory) CreateLogin(emailVerifyHash, passwordHash string, fullName string, company string, pictureURL string) (*UserLogin, error) { - user := m.getUserByEmailVerifyHash(emailVerifyHash) +// This method needs to be fixed to work with the new data model using LDAP +func (m *backendMemory) CreateLogin(email, passwordHash string, fullName string) (*UserLogin, error) { + user := m.getUserByEmail(email) if user == nil { return nil, errUserNotFound } user.FullName = fullName - //user.CompanyId = ??? m.LastLoginID = m.LastLoginID + 1 login := UserLogin{m.LastLoginID, user.UserID, 1, passwordHash} @@ -147,13 +157,16 @@ func (m *backendMemory) CreateLogin(emailVerifyHash, passwordHash string, fullNa func (m *backendMemory) UpdateEmailAndInvalidateSessions(email string, password string, newEmail string) (*UserLoginSession, error) { return nil, nil } + func (m *backendMemory) UpdatePasswordAndInvalidateSessions(email string, oldPassword string, newPassword string) (*UserLoginSession, error) { return nil, nil } + func (m *backendMemory) InvalidateSession(sessionHash string) error { m.removeSession(sessionHash) return nil } + func (m *backendMemory) InvalidateRememberMe(selector string) error { m.removeRememberMe(selector) return nil diff --git a/backendMemory_test.go b/backendMemory_test.go index 1a23156..fe7e510 100644 --- a/backendMemory_test.go +++ b/backendMemory_test.go @@ -156,17 +156,17 @@ func TestBackendVerifyEmail(t *testing.T) { func TestBackendUpdateUser(t *testing.T) { backend := NewBackendMemory().(*backendMemory) - backend.UpdateUser(nil, "fullname", "company", "pictureUrl") + backend.UpdateUser("emailHash", "fullname", "company", "pictureUrl") } func TestBackendCreateLogin(t *testing.T) { backend := NewBackendMemory().(*backendMemory) - if _, err := backend.CreateLogin("emailVerifyHash", "passwordHash", "fullName", "company", "pictureUrl"); err != errUserNotFound { + if _, err := backend.CreateLogin("email", "passwordHash", "fullName"); err != errUserNotFound { t.Error("expected login not found err", err) } backend.Users = append(backend.Users, &User{EmailVerifyHash: "emailVerifyHash", UserID: 1, PrimaryEmail: "email"}) - if login, err := backend.CreateLogin("emailVerifyHash", "passwordHash", "fullName", "company", "pictureUrl"); err != nil || login.LoginID != 1 || login.UserID != 1 { + if login, err := backend.CreateLogin("email", "passwordHash", "fullName"); err != nil || login.LoginID != 1 || login.UserID != 1 { t.Error("expected valid login", login) } } diff --git a/backendOnedb.go b/backendOnedb.go index 5d137a1..3b22893 100644 --- a/backendOnedb.go +++ b/backendOnedb.go @@ -1,9 +1,9 @@ package main import ( - "errors" + // "errors" "github.com/robarchibald/onedb" - "time" + // "time" ) type BackendOnedb struct { @@ -26,6 +26,7 @@ type BackendOnedb struct { InvalidateUserSessionsQuery string } +/* func (b *BackendOnedb) GetUserLogin(email, loginProvider string) (*UserLogin, error) { var login *UserLogin return login, b.Db.QueryStruct(onedb.NewSqlQuery(b.GetUserLoginQuery, email, loginProvider), login) @@ -81,7 +82,7 @@ func (b *BackendOnedb) UpdateUser(session *UserLoginSession, fullname string, co return nil } -func (b *BackendOnedb) CreateLogin(email, passwordHash string, fullName string, company string, pictureUrl string, sessionHash string, sessionRenewTimeUTC, sessionExpireTimeUTC time.Time) (*UserLoginSession, error) { +func (b *BackendOnedb) CreateLogin(email, passwordHash string, fullName string, company string, pictureUrl string) (*UserLogin, error) { return nil, nil } @@ -100,3 +101,4 @@ func (b *BackendOnedb) InvalidateSession(sessionHash string) error { func (b *BackendOnedb) Close() error { return b.Db.Close() } +*/ diff --git a/backend_test.go b/backend_test.go index d013812..698f9ce 100644 --- a/backend_test.go +++ b/backend_test.go @@ -69,14 +69,17 @@ func (b *MockBackend) GetLogin(email, loginProvider string) (*UserLogin, error) b.MethodsCalled = append(b.MethodsCalled, "GetUserLogin") return b.GetUserLoginReturn.Login, b.GetUserLoginReturn.Err } + func (b *MockBackend) GetSession(sessionHash string) (*UserLoginSession, error) { b.MethodsCalled = append(b.MethodsCalled, "GetSession") return b.GetSessionReturn.Session, b.GetSessionReturn.Err } + func (b *MockBackend) CreateSession(loginID, userID int, sessionHash string, sessionRenewTimeUTC, sessionExpireTimeUTC time.Time, rememberMe bool, rememberMeSelector, rememberMeTokenHash string, rememberMeRenewTimeUTC, rememberMeExpireTimeUTC time.Time) (*UserLoginSession, *UserLoginRememberMe, error) { b.MethodsCalled = append(b.MethodsCalled, "NewLoginSession") return b.NewLoginSessionReturn.Session, b.NewLoginSessionReturn.RememberMe, b.NewLoginSessionReturn.Err } + func (b *MockBackend) RenewSession(sessionHash string, renewTimeUTC time.Time) (*UserLoginSession, error) { b.MethodsCalled = append(b.MethodsCalled, "RenewSession") return b.RenewSessionReturn.Session, b.RenewSessionReturn.Err @@ -99,12 +102,12 @@ func (b *MockBackend) VerifyEmail(emailVerifyHash string) (string, error) { return b.VerifyEmailReturn.Email, b.VerifyEmailReturn.Err } -func (b *MockBackend) UpdateUser(session *UserLoginSession, fullname string, company string, pictureURL string) error { +func (b *MockBackend) UpdateUser(emailVerifyHash, fullname string, company string, pictureURL string) (string, error) { b.MethodsCalled = append(b.MethodsCalled, "UpdateUser") - return b.ErrReturn + return "test@test.com", b.ErrReturn } -func (b *MockBackend) CreateLogin(email string, passwordHash string, fullName string, company string, pictureURL string) (*UserLogin, error) { +func (b *MockBackend) CreateLogin(email, passwordHash, fullName string) (*UserLogin, error) { b.MethodsCalled = append(b.MethodsCalled, "CreateLogin") return b.CreateLoginReturn.Login, b.CreateLoginReturn.Err } diff --git a/cryptoStore.go b/cryptoStore.go new file mode 100644 index 0000000..5a292d8 --- /dev/null +++ b/cryptoStore.go @@ -0,0 +1,75 @@ +package main + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" +) + +func decodeStringToHash(token string) (string, error) { + data, err := decodeFromString(token) + if err != nil { + return "", err + } + return encodeToString(hash(data)), nil +} + +func decodeFromString(token string) ([]byte, error) { + return base64.URLEncoding.DecodeString(token) +} + +func encodeToString(bytes []byte) string { + return base64.URLEncoding.EncodeToString(bytes) +} + +func generateSelectorTokenAndHash() (string, string, string, error) { + var selector, token, tokenHash string + selector, err := generateRandomString() + if err != nil { + return "", "", "", newLoggedError("Unable to generate rememberMe selector", err) + } + token, tokenHash, err = generateStringAndHash() + if err != nil { + return "", "", "", newLoggedError("Unable to generate rememberMe token", err) + } + return selector, token, tokenHash, nil +} + +func generateStringAndHash() (string, string, error) { + b, err := generateRandomBytes(32) + if err != nil { + return "", "", err + } + return encodeToString(b), encodeToString(hash(b)), nil +} + +func hash(bytes []byte) []byte { + h := sha256.Sum256(bytes) + return h[:] +} + +// Url decode both the token and the hash and then compare +func encodedHashEquals(token, tokenHash string) bool { + tokenBytes, _ := decodeFromString(token) + hashBytes, _ := decodeFromString(tokenHash) + return hashEquals(tokenBytes, hashBytes) +} + +func hashEquals(token, tokenHash []byte) bool { + return subtle.ConstantTimeCompare(hash(token), tokenHash) == 1 +} + +func generateRandomString() (string, error) { + bytes, err := generateRandomBytes(32) + return encodeToString(bytes), err +} + +func generateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + return b, nil +} diff --git a/loginStore.go b/loginStore.go new file mode 100644 index 0000000..2ad85cd --- /dev/null +++ b/loginStore.go @@ -0,0 +1,75 @@ +package main + +import ( + "net/http" +) + +type LoginStorer interface { + Login(email, password string, rememberMe bool) (*UserLogin, error) + LoginBasic() (*UserLogin, error) + + CreateLogin(email, fullName, password string) (*UserLogin, error) + UpdateEmail() error + UpdatePassword() error +} + +type loginStore struct { + backend BackendQuerier + mailer Mailer + r *http.Request +} + +func NewLoginStore(backend BackendQuerier, mailer Mailer, r *http.Request) LoginStorer { + return &loginStore{backend, mailer, r} +} + +func (s *loginStore) LoginBasic() (*UserLogin, error) { + if email, password, ok := s.r.BasicAuth(); ok { + login, err := s.Login(email, password, false) + if err != nil { + return nil, newLoggedError("Unable to login with provided credentials", err) + } + return login, nil + } else { + return nil, newAuthError("Problem decoding credentials from basic auth", nil) + } +} + +func (s *loginStore) Login(email, password string, rememberMe bool) (*UserLogin, error) { + if !isValidEmail(email) { + return nil, newAuthError("Please enter a valid email address.", nil) + } + if !isValidPassword(password) { + return nil, newAuthError(passwordValidationMessage, nil) + } + + login, err := s.backend.GetLogin(email, loginProviderDefaultName) + if err != nil { + return nil, newLoggedError("Invalid username or password", err) + } + + decoded, _ := decodeFromString(login.ProviderKey) + if !hashEquals([]byte(password), decoded) { + return nil, newLoggedError("Invalid username or password", nil) + } + return login, nil +} + +func (s *loginStore) CreateLogin(email, fullName, password string) (*UserLogin, error) { + passwordHash := encodeToString(hash([]byte(password))) + login, err := s.backend.CreateLogin(email, passwordHash, fullName) + if err != nil { + return nil, newLoggedError("Unable to create login", err) + } + return login, err +} + +func (s *loginStore) UpdateEmail() error { return nil } + +func (s *loginStore) UpdatePassword() error { + return nil +} + +func isValidPassword(password string) bool { + return len(password) >= 7 && len(password) <= 20 +} diff --git a/loginStore_test.go b/loginStore_test.go new file mode 100644 index 0000000..c715d1d --- /dev/null +++ b/loginStore_test.go @@ -0,0 +1,112 @@ +package main + +import ( + "net/http" + "testing" +) + +func getLoginStore(mailErr error, backend *MockBackend) LoginStorer { + r := &http.Request{} + return &loginStore{backend, &TextMailer{Err: mailErr}, r} +} + +func TestNewLoginStore(t *testing.T) { + r := &http.Request{} + b := &MockBackend{} + m := &TextMailer{} + actual := NewLoginStore(b, m, r).(*loginStore) + if actual.backend != b { + t.Fatal("expected correct init") + } +} + +var loginTests = []struct { + Scenario string + Email string + Password string + RememberMe bool + CreateSessionReturn *SessionReturn + GetUserLoginReturn *LoginReturn + ErrReturn error + MethodsCalled []string + ExpectedResult *UserLoginRememberMe + ExpectedErr string +}{ + { + Scenario: "Invalid email", + Email: "invalid@bogus", + ExpectedErr: "Please enter a valid email address.", + }, + { + Scenario: "Invalid password", + Email: "email@example.com", + Password: "short", + ExpectedErr: passwordValidationMessage, + }, + { + Scenario: "Can't get login", + Email: "email@example.com", + Password: "validPassword", + GetUserLoginReturn: loginErr(), + MethodsCalled: []string{"GetUserLogin"}, + ExpectedErr: "Invalid username or password", + }, + { + Scenario: "Incorrect password", + Email: "email@example.com", + Password: "wrongPassword", + GetUserLoginReturn: &LoginReturn{Login: &UserLogin{LoginID: 1, UserID: 1, ProviderKey: "1234"}}, + MethodsCalled: []string{"GetUserLogin"}, + ExpectedErr: "Invalid username or password", + }, + { + Scenario: "Got session", + Email: "email@example.com", + Password: "correctPassword", + GetUserLoginReturn: loginSuccess(), + CreateSessionReturn: sessionSuccess(futureTime, futureTime), + MethodsCalled: []string{"GetUserLogin"}, + }, +} + +func TestAuthLogin(t *testing.T) { + for i, test := range loginTests { + backend := &MockBackend{GetUserLoginReturn: test.GetUserLoginReturn, ErrReturn: test.ErrReturn} + store := getLoginStore(nil, backend).(*loginStore) + val, err := store.Login(test.Email, test.Password, test.RememberMe) + methods := store.backend.(*MockBackend).MethodsCalled + if (err == nil && test.ExpectedErr != "" || err != nil && test.ExpectedErr != err.Error()) || + !collectionEqual(test.MethodsCalled, methods) { + t.Errorf("Scenario[%d] failed: %s\nexpected err:%v\tactual err:%v\nexpected val:%v\tactual val:%v\nexpected methods: %s\tactual methods: %s", i, test.Scenario, test.ExpectedErr, err, test.ExpectedResult, val, test.MethodsCalled, methods) + } + } +} + +/****************************************************************************/ +type MockLoginStore struct { + LoginReturn *LoginReturn +} + +func NewMockLoginStore() LoginStorer { + return &MockLoginStore{} +} + +func (s *MockLoginStore) Login(email, password string, rememberMe bool) (*UserLogin, error) { + return s.LoginReturn.Login, s.LoginReturn.Err +} + +func (s *MockLoginStore) LoginBasic() (*UserLogin, error) { + return s.LoginReturn.Login, s.LoginReturn.Err +} + +func (s *MockLoginStore) CreateLogin(email, fullName, password string) (*UserLogin, error) { + return s.LoginReturn.Login, s.LoginReturn.Err +} + +func (s *MockLoginStore) UpdateEmail() error { + return s.LoginReturn.Err +} + +func (s *MockLoginStore) UpdatePassword() error { + return s.LoginReturn.Err +}