DEV Community

loading...
Cover image for 15 - Add Signin to Service and Repository Layers

15 - Add Signin to Service and Repository Layers

jacobsngoodwin profile image Jacob Goodwin ・5 min read

Last time, we created and tested a handler for signing in a user. This handler accepts and validates an email and password received in a JSON request body. Let's take a look at our progress diagram to see what we'll work on today!

15 - Signin Service and Repository Layers

We'll add functionality to sign-in a user in the service layer. To do this, we'll need to be able to find a user in our database by email, and then verify that the password entered by the user matches the encrypted stored password from the database. To make this possible, we'll add a FindByEmail method to our UserRepository interface, mock, and Postgres implementations. This method will retrieve the user attempting to sign-in, which includes their hashed password. We'll also review and use the function we previously created to compare the user's supplied password with the password stored in the database.

At the end of the tutorial, we'll spin up the application, create a new user with a known password, and then attempt to sign in as that user.

As always, checkout the repo on Github with all of the code for this tutorial including complete tests a branch for each lesson!

If you prefer video, check out the video version below!

I will continue to create unit tests for handler- and service-layer methods. However, I will not include these in written or video tutorials unless I feel they provide some new information. I feel a lengthy portion of the tutorials has been dedicated to testing. I did this deliberately because sometimes setting up tests is more difficult than actual coding logic (seriously), and only a few tutorials write more than rudimentary tests.

Add FindByEmail to UserRepository Interface

In ~/model/interfaces.go, let's add an expectation that a UserRepository can find a user by email address.

// UserRepository defines methods the service layer expects
// any repository it interacts with to implement
type UserRepository interface {
    Create(ctx context.Context, u *User) error
    FindByEmail(ctx context.Context, email string) (*User, error)
    FindByID(ctx context.Context, uid uuid.UUID) (*User, error)
}
Enter fullscreen mode Exit fullscreen mode

You should now have errors because or pGUserRepository and mockUserRepository do not implement this newly added method. Let's add these implementations!

Add FindByEmail to mockUserRepository

Though I mentioned that I won't go over unit tests unless necessary, I'll still add the mock implementations so you don't have any errors.

Add the following to ~/model/mocks/user_repository.go.

// FindByEmail is mock of UserRepository.FindByEmail
func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
    ret := m.Called(ctx, email)

    var r0 *model.User
    if ret.Get(0) != nil {
        r0 = ret.Get(0).(*model.User)
    }

    var r1 error

    if ret.Get(1) != nil {
        r1 = ret.Get(1).(error)
    }

    return r0, r1
}
Enter fullscreen mode Exit fullscreen mode

Add FindByEmail to pGUserRepository

Let's now add the implementation to our the Postgres User Repository implementation, found in ~/repository/pg_user_repository.go.

// FindByEmail retrieves user row by email address
func (r *pGUserRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
    user := &model.User{}

    query := "SELECT * FROM users WHERE email=$1"

    if err := r.DB.GetContext(ctx, user, query, email); err != nil {
        log.Printf("Unable to get user with email address: %v. Err: %v\n", email, err)
        return user, apperrors.NewNotFound("email", email)
    }

    return user, nil
}
Enter fullscreen mode Exit fullscreen mode

In this code block, we simply create a select query to get a user with the given email, and return a NotFound error if the user cannot be found (and for all possible errors, which is maybe a bit lazy). If the user is found, it will be populated on the *model.User and returned.

User Service Signin Implementation

We can now reach out to FindByEmail within the UserService. Let's add this in ~/service/user_service.go!

// Signin reaches our to a UserRepository check if the user exists
// and then compares the supplied password with the provided password.
// If a valid email/password combo is provided, u will hold all
// available user fields
func (s *userService) Signin(ctx context.Context, u *model.User) error {
    uFetched, err := s.UserRepository.FindByEmail(ctx, u.Email)

    // Will return NotAuthorized to client to omit details of why
    if err != nil {
        return apperrors.NewAuthorization("Invalid email and password combination")
    }

    // verify password - we previously created this method
    match, err := comparePasswords(uFetched.Password, u.Password)

    if err != nil {
        return apperrors.NewInternal()
    }

    if !match {
        return apperrors.NewAuthorization("Invalid email and password combination")
    }

    u = uFetched
    return nil
}
Enter fullscreen mode Exit fullscreen mode

After storing the user in uFetch, we make a call to comparePasswords which compares the supplied password (from the HTTP request) and the retrieved password (from the database). We previously created this function in ~/service/passwords.go. This function extracts the password salt, pwsalt[1], and then hashes the supplied password with this salt. If this matches the hashed password from the database, pwsalt[0], we know the user entered a valid password! We return whether or not the password is a match as a boolean. We can also return an error if decoding the stored password fails.

func comparePasswords(storedPassword string, suppliedPassword string) (bool, error) {
    pwsalt := strings.Split(storedPassword, ".")

    // check supplied password salted with hash
    salt, err := hex.DecodeString(pwsalt[1])

    if err != nil {
        return false, fmt.Errorf("Unable to verify user password")
    }

    shash, err := scrypt.Key([]byte(suppliedPassword), salt, 32768, 8, 1, 32)

    return hex.EncodeToString(shash) == pwsalt[0], nil
}
Enter fullscreen mode Exit fullscreen mode

Run Application

From the root of the project, you should now be able to run docker-compose up! Let's create a new user by posting a request to our signup endpoint (I've deleted all previous users from Postgres prior to this tutorial). I'll use curl here to keep things simple, and Postman in the video if you prefer to check that out!

➜  curl --location --request POST 'http://malcorp.test/api/account/signup' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "guy01@guy.com",
    "password": "avalidpassword123"
}'
Enter fullscreen mode Exit fullscreen mode

We get a status 201, created, response with the following body.

HTTP/1.1 201 Created
Content-Length: 871
Content-Type: application/json; charset=utf-8
Date: Wed, 23 Dec 2020 01:39:09 GMT

{"tokens":{"idToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVpZCI6ImVlODZjYzJjLTg2ODEtNDU5YS1hOTc3LTNjYWY5NzUzZTE0YiIsImVtYWlsIjoiZ3V5MDFAZ3V5LmNvbSIsIm5hbWUiOiIiLCJpbWFnZVVybCI6IiIsIndlYnNpdGUiOiIifSwiZXhwIjoxNjA4Njg4NDQ5LCJpYXQiOjE2MDg2ODc1NDl9.haU3a-15xfoUYrpllkkuUphKFDqNfZKckmPZP6LRN7BGhe15DAONdirhLnH1n5QHFaqQ31eOs1nAleqln5MTzeG_YYdw4VhbQ53wve_b156SeMEvfm664js8fSQYsfTG_PBzkmkRaL62jcSaNmSWkKhzzT5bBeYlBd4lUBqGV1nw12Jj9WgF6oWoDHN786bSMQz25TWmkVyE1-082DHUdAjqnnVy7J_G-CU1Ozdv_6KUurUeVfqBj0D4irghcMnfnk75vBzFhyOShl2-RkXprRKvqjNo0u28Fd5BZ6ZKLAv6k_iUxK8rb-F3atozlFhdWaNL77w18XI4ZkZ2YieLPw","refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJlZTg2Y2MyYy04NjgxLTQ1OWEtYTk3Ny0zY2FmOTc1M2UxNGIiLCJleHAiOjE2MDg5NDY3NDksImp0aSI6IjIzODhkY2Y5LWVlODQtNGUzMy04ZGJlLTZmNTlkOGQ2NzFmNCIsImlhdCI6MTYwODY4NzU0OX0.Rsr2zFf62WYm2ExZXo5zVlq0Ot_jqnoUtL8xapjc75U"}}%
Enter fullscreen mode Exit fullscreen mode

If we then send a request to the signin endpoint with the same email and password, we should hopefully get a token pair and a response of status 200 (ok).

➜ curl -i --location --request POST 'http://malcorp.test/api/account/signin' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "guy01@guy.com",
    "password": "avalidpassword123"
}'
HTTP/1.1 200 OK
Content-Length: 871
Content-Type: application/json; charset=utf-8
Date: Wed, 23 Dec 2020 01:39:51 GMT

{"tokens":{"idToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVpZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsImVtYWlsIjoiZ3V5MDFAZ3V5LmNvbSIsIm5hbWUiOiIiLCJpbWFnZVVybCI6IiIsIndlYnNpdGUiOiIifSwiZXhwIjoxNjA4Njg4NDkxLCJpYXQiOjE2MDg2ODc1OTF9.YuGGc6m1ZaL7BMSGTwdS7hBt8QFIcxRn1MJ-PqjnOm9vtVUPrVsYbg0n_TcwypcqtAcuhsI3buIipFj9GJU657q3INZWcVzNzlWEzeaPPUKuoJtL2EUP6veGElKd8bAQWsg5eX1T48ff8x4CxW-s7PJ0ZLWMi2Al2TU4xbzz4wxGs6PfgD3T4UYuwnCvnC3GGRdL0htLmqc9EiGqs4M6fzu8HhrusKSRvDdbbKNBO6eELtOzRM8_YKcbBBKMGsS9gKxGnDY227_zqYsc1T1fpy7NYmz7SSxZjd4c6XEqcmItqG28L9tvELZk1HlMQvOI7_yTxW13ntCaLLKdkKnZ0A","refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJleHAiOjE2MDg5NDY3OTEsImp0aSI6IjQ3MTFjMDE0LTVmMDgtNDZhNC04Njk3LWQwNGVjMDMwMTEyMCIsImlhdCI6MTYwODY4NzU5MX0.aG7Y01RldyOxxkmaFIT04iYhvb1joSkBw0bboIDSpmE"}}% 
Enter fullscreen mode Exit fullscreen mode

Conclusion

We now have the ability to sign up and sign in users. This was quite an effort, especially since we added unit tests and spent a great deal of time on architecture.

Next time, we'll create our second middleware, which will be used to extract the user from our idToken. You might recall that at the very beginning of the tutorial, we created a me handler and API endpoint, but didn't use this in our application. I did this deliberately to show that we could test the handler in isolation od middleware and service layers.

Next time, we'll create this middleware. This can be used to verify the user, and authorize them to do things like retrieve and update their user profile details.

Until next time, ¡chau!

Discussion (0)

pic
Editor guide