DEV Community

loading...
Cover image for 07 - Completing Signup Handler in Gin - Token Creation

07 - Completing Signup Handler in Gin - Token Creation

jacobsngoodwin profile image Jacob Goodwin ・8 min read

Last time, we started building a handler for signing up a user with an email address and password. We learned how to validate and bind incoming JSON on an HTTP request body to a struct, and then how to call the UserService for signing up a user.

Accessing Token Service

In this tutorial, we'll complete this handler by having it reach out to a TokenService which creates an access token and a refresh token. These tokens will be created, or derived, from the User we signed up in the last tutorial. These tokens are what we'll use to authorize users to the eventual memorization app we plan to build. In a real-word scenario, these tokens could be used to access various applications or micro-services in a company or organization.

At the end of this section, your folder structure should be as follows. If at any point you are confused about the file structure or code, go to Github repository and checkout the branch for the previous lesson!

Folder Structure

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

I want to clarify that we'll definitely go into our auth flow later. For now we're going to focus on the handler logic. I've added some resources at the end of the article that I used in deciding how to store tokens in this app. This is merely a part of the auth flow decision process, but I hope you'll find the info useful.

Add Tokens Model

We need to create a model for the structure of the aforementioned tokens that we'll send to users as JSON. Recall that we create these models in our model layer, and that these models can be used across, and passed between, application layers. Right now we have a single User model.

Let's add a model for our access and refresh tokens in ~/model/token_pair.go. In this app, the ID token will also serve as an access token (for those of you that are thinking ahead). The tokens we ultimately serialize and send to the user as JSON with the keys in the struct tags are represented as strings.

// TokenPair used for returning pairs of id and refresh tokens
type TokenPair struct {
    IDToken      string `json:"idToken"`
    RefreshToken string `json:"refreshToken"`
}
Enter fullscreen mode Exit fullscreen mode

Create TokenService Interface

With the TokenPair model created, we now define our expectations for the TokenService. Let's do this by adding this TokenService to ~/model/interfaces.go, and add our first method for creating tokens from a User.

// TokenService defines methods the handler layer expects to interact
// with in regards to producing JWTs as string
type TokenService interface {
    NewPairFromUser(ctx context.Context, u *User, prevTokenID string) (*TokenPair, error)
}
Enter fullscreen mode Exit fullscreen mode

The method accepts a Context from the handler layer, a User, and a prevTokenID. When we sign up a user, we'll be passing an empty string to prevTokenID. We'll eventually create a refresh token handler which will call this method with a non-empty prevTokenID.

Add TokenService to Handler Struct

We also need to make sure we have access to this TokenService in our handler. Inside of ~/handler/handler.go, let's add it to our Handler and Config structs. We also need to make sure to "instantiate" our handler inside of NewHandler with the TokenService passed from the config.

// Handler struct holds required services for handler to function
type Handler struct {
    UserService  model.UserService
    TokenService model.TokenService
}

// Config will hold services that will eventually be injected into this
// handler layer on handler initialization
type Config struct {
    R            *gin.Engine
    UserService  model.UserService
    TokenService model.TokenService
}

// NewHandler initializes the handler with required injected services along with http routes
// Does not return as it deals directly with a reference to the gin Engine
func NewHandler(c *Config) {
    // Create a handler (which will later have injected services)
    h := &Handler{
        UserService:  c.UserService,
        TokenService: c.TokenService,
  }

  ...
Enter fullscreen mode Exit fullscreen mode

Create Tokens in Signup

Let's call this method back inside of ~/handler/signup.go. I've also included some of the code from the last tutorial where we called UserService.Signup.

...
u := &model.User{
        Email:    req.Email,
        Password: req.Password,
    }

    err := h.UserService.Signup(c, u)

    if err != nil {
        log.Printf("Failed to sign up user: %v\n", err.Error())
        c.JSON(apperrors.Status(err), gin.H{
            "error": err,
        })
        return
    }

    // create token pair as strings
    tokens, err := h.TokenService.NewPairFromUser(c, u, "")

    if err != nil {
        log.Printf("Failed to create tokens for user: %v\n", err.Error())

        // may eventually implement rollback logic here
        // meaning, if we fail to create tokens after creating a user,
        // we make sure to clear/delete the created user in the database

        c.JSON(apperrors.Status(err), gin.H{
            "error": err,
        })
        return
    }

    c.JSON(http.StatusCreated, gin.H{
        "tokens": tokens,
    })
Enter fullscreen mode Exit fullscreen mode

We pass the reference to the successfully created User to the NewPairFromUser method. If there is an error, we're return this error as JSON. If the token creation is successful, we'll send the pair as JSON according to the struct tags in model.TokenPair.

Mock TokenService.NewPairFromUser Method

Before we can test the token-creation portion of Signup, we need to create a mock implementation of NewPairFromUser. As always, take a look at previous lectures and the testify documentation to learn more!

Let's create a new file, ~/model/mocks/token_service.go, remembering to include appropriate imports for your module.

// MockTokenService is a mock type for model.TokenService
type MockTokenService struct {
    mock.Mock
}

// NewPairFromUser mocks concrete NewPairFromUser
func (m *MockTokenService) NewPairFromUser(ctx context.Context, u *model.User, prevTokenID string) (*model.TokenPair, error) {
    ret := m.Called(ctx, u, prevTokenID)

    // first value passed to "Return"
    var r0 *model.TokenPair
    if ret.Get(0) != nil {
        // we can just return this if we know we won't be passing function to "Return"
        r0 = ret.Get(0).(*model.TokenPair)
    }

    var r1 error

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

    return r0, r1
}
Enter fullscreen mode Exit fullscreen mode

Complete Tests of Signup Handler

With the mock created, we're now ready to add at least two more test cases.

  1. Successful token creation upon calling NewPairFromUser
  2. An error returned upon calling NewPairFromUser

For both of these cases, we'll need to create a mock for both the UserService and TokenService. Calling UserService.Signup should not return an error for both of these tests, but calling TokenService.NewPairFromUser should return a different result for the two cases.

Successful Token Creation

Let's note the important aspects of this test.

  1. We create a user, u, which will be created when calling mockUserService.Signup. We also create a mockTokenResponse to return when calling mockTokenService.NewPairFromUser.
  2. We create the mock services mentioned in 1, and define responses for when their methods are called. As this is the success case, we return mockTokenResponse, nil when calling NewPairFromUser.
  3. We setup our handler, remembering to inject mockTokenService into the NewHandler factory.
  4. We setup our request body with valid email and password as in previous tests.
  5. We send the POST request with reqBody to /signup, and define our expectations. We expect the methods on both services to be called, and we expect to receive an HTTP status code of 201, indicating a resource has been created. We also check the response body of the returned JSON.
t.Run("Successful Token Creation", func(t *testing.T) {
        u := &model.User{
            Email:    "bob@bob.com",
            Password: "avalidpassword",
        }

        mockTokenResp := &model.TokenPair{
            IDToken:      "idToken",
            RefreshToken: "refreshToken",
        }

        mockUserService := new(mocks.MockUserService)
        mockTokenService := new(mocks.MockTokenService)

        mockUserService.
            On("Signup", mock.AnythingOfType("*gin.Context"), u).
            Return(nil)
        mockTokenService.
            On("NewPairFromUser", mock.AnythingOfType("*gin.Context"), u, "").
            Return(mockTokenResp, nil)

        // a response recorder for getting written http response
        rr := httptest.NewRecorder()

        // don't need a middleware as we don't yet have authorized user
        router := gin.Default()

        NewHandler(&Config{
            R:            router,
            UserService:  mockUserService,
            TokenService: mockTokenService,
        })

        // create a request body with empty email and password
        reqBody, err := json.Marshal(gin.H{
            "email":    u.Email,
            "password": u.Password,
        })
        assert.NoError(t, err)

        // use bytes.NewBuffer to create a reader
        request, err := http.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
        assert.NoError(t, err)

        request.Header.Set("Content-Type", "application/json")

        router.ServeHTTP(rr, request)

        respBody, err := json.Marshal(gin.H{
            "tokens": mockTokenResp,
        })
        assert.NoError(t, err)

        assert.Equal(t, http.StatusCreated, rr.Code)
        assert.Equal(t, respBody, rr.Body.Bytes())

        mockUserService.AssertExpectations(t)
        mockTokenService.AssertExpectations(t)
    })
Enter fullscreen mode Exit fullscreen mode

Error Token Creation

Much of this test is the same as the last, with the following changes.

  1. We replace mockTokenResponse with mockErrorResponse, and change the response of mockTokenService.NewPairFromUser to nil, mockErrorResponse.
  2. We change the expectations for the handler's JSON response. We expect the body to have an error key containing mockErrorResponse, and we expect the status code to be that contained in the mockErrorResponse, which for this test we created using one of our custom appErrors for an internal, http status code 500, error.
t.Run("Failed Token Creation", func(t *testing.T) {
        u := &model.User{
            Email:    "bob@bob.com",
            Password: "avalidpassword",
        }

        mockErrorResponse := apperrors.NewInternal()

        mockUserService := new(mocks.MockUserService)
        mockTokenService := new(mocks.MockTokenService)

        mockUserService.
            On("Signup", mock.AnythingOfType("*gin.Context"), u).
            Return(nil)
        mockTokenService.
            On("NewPairFromUser", mock.AnythingOfType("*gin.Context"), u, "").
            Return(nil, mockErrorResponse)

        // a response recorder for getting written http response
        rr := httptest.NewRecorder()

        // don't need a middleware as we don't yet have authorized user
        router := gin.Default()

        NewHandler(&Config{
            R:            router,
            UserService:  mockUserService,
            TokenService: mockTokenService,
        })

        // create a request body with empty email and password
        reqBody, err := json.Marshal(gin.H{
            "email":    u.Email,
            "password": u.Password,
        })
        assert.NoError(t, err)

        // use bytes.NewBuffer to create a reader
        request, err := http.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
        assert.NoError(t, err)

        request.Header.Set("Content-Type", "application/json")

        router.ServeHTTP(rr, request)

        respBody, err := json.Marshal(gin.H{
            "error": mockErrorResponse,
        })
        assert.NoError(t, err)

        assert.Equal(t, mockErrorResponse.Status(), rr.Code)
        assert.Equal(t, respBody, rr.Body.Bytes())

        mockUserService.AssertExpectations(t)
        mockTokenService.AssertExpectations(t)
    })
Enter fullscreen mode Exit fullscreen mode

Conclusion

Thanks for joining me again! I hope you've learned something today. If you didn't, you wouldn't be the first person of superior intelligence and knowledge I've encountered, and I'm fine with that.😁 But also thanks for reading this far!

Next time, we'll move onto the service layer, and eventually to the repository layer with the hope that we'll soon(ish) be able to signup users and store them in an actual database by making requests with an HTTP client.

¡Hasta pronto¡

Bonus: Auth Information

For those of you with some experience creating auth flows, I'm including some links here for you that address the complexity of storing "tokens" (in whatever form and via whatever storage mechanism). I do this so that you'll understand that I did not choose blindly the auth mechanism for this project. You may disagree with my approach, but I still encourage you to read the resources below to refine your views (whether I sway you, or push you to more vehemently disagree, I just hope you also lose as much sleep as I did mulling this over).

Please Stop Using Local Storage - Note, I highly recommend reading the comments by Jon Gross-Dubois, if not all of the comments.

Academind Explanation

Auth0 - Note that their JS/SPA APIs basically have the option of storing tokens in memory or in local storage, though they give a scary warning about using local storage. And this is a company of people that sit around (though standing desks are cool) thinking about this way more than most of us.

SPA Best Practices - Cookie session ID's are basically just a "token."

Of course, you could also go read 50 page theses written by some of the finest cryptography academicians in unreadable plaintext, which basically tell you you're screwed, all the while providing no workable solution for the "normies" like you and me. (On a hopeful note, I did see some promising implementations coming down the road. 🤞🏼)

Does your head hurt yet? Anyhow, if you lose sleep over this, I hope you'll find comfort in knowing that I, your fellow developer, have also lost sleep over this.

Discussion (0)

pic
Editor guide