Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 24, 2021 08:13 am GMT

Implement login user API that returns PASETO or JWT access token in Go

Hello everyone! Welcome back to the backend master class!

In the previous lecture, weve implemented the token maker interface using JWT and PASETO. It provides 2 methods to create and verify tokens.

So today were gonna learn how to use it to implement the login API, where the username and password are provided by the client, and the server will return an access token if those credentials are correct.

Here's:

OK lets start!

Add token Maker to the Server

The first step is to add the token maker to our API server. So lets open api/server.go file!

In the Server struct, Im gonna add a tokenMaker field of type token.Maker interface.

type Server struct {    store      db.Store    tokenMaker token.Maker    router     *gin.Engine}

Then lets initialize this field inside the NewServer() function! First we have to create a new token maker object. We can choose to use either JWT or PASETO, they both implement the same token.Maker interface.

I think PASETO is better, so lets call token.NewPasetoMaker(). It requires a symmetric key string, so we will need to load this from environment variable. For now, lets just put an empty string here as a placeholder.

If the returned error is not nil, we return a nil server, and an error saying "cannot create token maker". The %w is used to wrap the original error.

func NewServer(store db.Store) (*Server, error) {    tokenMaker, err := token.NewPasetoMaker("")    if err != nil {        return nil, fmt.Errorf("cannot create token maker: %w", err)    }    server := &Server{        store:      store,        tokenMaker: tokenMaker,    }    ...    return server, nil}

OK, so now we have to change the return type of the NewServer() function to include an error as well. Then in the statement to create a Server object, we add the tokenMaker object that weve just created.

Alright, now lets come back to the symmetric key parameter. Im gonna add a new environment variable to the app.env file. Lets call it TOKEN_SYMMETRIC_KEY.

And as were using PASETO version 2, which uses ChachaPoly algorithm, the size of this symmetric key should be exactly 32 bytes.

TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012ACCESS_TOKEN_DURATION=15m

We should also add 1 more variable to store the valid duration of the access token. Its a best practice to set this to a very short duration, lets say, just 15 minutes for example.

OK, now we have to update our config struct to include the 2 new variables that weve just added.

First, the TokenSymmetricKey of type string. We have to specify the mapstructure tag for it because viper uses mapstructure package to parse the config data. Please refer to the lecture 12 of the course if you dont know how to use viper.

type Config struct {    ...    TokenSymmetricKey   string        `mapstructure:"TOKEN_SYMMETRIC_KEY"`    AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"`}

The next field is AccessTokenDuration of type time.Duration. And its mapstructure tag should be this environment variables name: ACCESS_TOKEN_DURATION.

As you can see, when the type of a config field is time.Duration, we can specify the value in a human readable format like this: 15m.

OK so now weve loaded the secret key and token duration into the config, lets go back to the server and use them. We have to add a config parameter to the NewServer() function. Then in the token.NewPasetoMaker() call, we pass in config.TokenSymmetricKey.

We should also add a config field to the Server struct, and store it here when initialize the Server object. We will use the TokenDuration in this config object later when creating the tokens.

type Server struct {    config     util.Config    store      db.Store    tokenMaker token.Maker    router     *gin.Engine}func NewServer(config util.Config, store db.Store) (*Server, error) {    tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey)    if err != nil {        return nil, fmt.Errorf("cannot create token maker: %w", err)    }    server := &Server{        config:     config,        store:      store,        tokenMaker: tokenMaker,    }    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {        v.RegisterValidation("currency", validCurrency)    }    server.setupRouter()    return server, nil}

At the end of this function, we should return a nil error. And that will be it!

However, as we added a new config parameter to the NewServer() function, some unit tests that we wrote before are broken. So lets fix them!

Fix broken unit tests

In the api/main_test.go file, Im gonna define a function newTestServer() that will create a new server for test. It takes a testing.T object and a db.Store interface as input. And it will return a Server object as output.

In this function, lets create a new config object, with TokenSymmetricKey is util.RandomString of 32 characters, and AccessTokenDuration is 1 minute.

func newTestServer(t *testing.T, store db.Store) *Server {    config := util.Config{        TokenSymmetricKey:   util.RandomString(32),        AccessTokenDuration: time.Minute,    }    server, err := NewServer(config, store)    require.NoError(t, err)    return server}

Then we create a new server with that config object and the input store interface. Require no errors, and finally return the created server.

Now get back to the api/transfer_test.go file. Here, instead of NewServer(), we will call newTestServer, and pass in the testing.T object and the mock store.

func TestTransferAPI(t *testing.T) {    ...    for i := range testCases {        tc := testCases[i]        t.Run(tc.name, func(t *testing.T) {            ...            server := newTestServer(t, store)            recorder := httptest.NewRecorder()            ...        })    }}

We do the same for the server inside api/user_test.go file and api/account_test.go file as well. There are several calls of NewServer() in these files, so we have to change all of them to newTestServer().

Alright, now everything is updated. Lets run the whole api package tests!

Alt Text

All passed! Excellent! So the tests are now working well with the new Server struct.

But theres one more place we need to update, thats the main entry point of our server: main.go

func main() {    config, err := util.LoadConfig(".")    if err != nil {        log.Fatal("cannot load config:", err)    }    conn, err := sql.Open(config.DBDriver, config.DBSource)    if err != nil {        log.Fatal("cannot connect to db:", err)    }    store := db.NewStore(conn)    server, err := api.NewServer(config, store)    if err != nil {        log.Fatal("cannot create server:", err)    }    err = server.Start(config.ServerAddress)    if err != nil {        log.Fatal("cannot start server:", err)    }}

Here, in this main() function, we have to add config to the api.NewServer() call. And this call will return a server and an error.

If error is not nil, we just write a fatal log, saying "cannot create server". Just like that, and were done!

Now its time to build the login user API!

Implement login user handler

Lets open the api/user.go file!

The login APIs request payload must contain the username and password, which is very similar to the createUserRequest:

type createUserRequest struct {    Username string `json:"username" binding:"required,alphanum"`    Password string `json:"password" binding:"required,min=6"`    FullName string `json:"full_name" binding:"required"`    Email    string `json:"email" binding:"required,email"`}

So Im gonna copy this struct, and paste it to the end of this file. Then lets change the struct name to loginUserRequest and remove the FullName and Email fields, just keep the Username and Password fields.

type loginUserRequest struct {    Username string `json:"username" binding:"required,alphanum"`    Password string `json:"password" binding:"required,min=6"`}

Next, lets define the loginUserResponse struct. The most important field that should be returned to the client is AccessToken string. This is the token that we will create using the token maker interface.

type loginUserResponse struct {    AccessToken string       `json:"access_token"`}

Beside the access token, we might also want to return some information of the logged in user, just like the one we returned in the create user API:

type createUserResponse struct {    Username          string    `json:"username"`    FullName          string    `json:"full_name"`    Email             string    `json:"email"`    PasswordChangedAt time.Time `json:"password_changed_at"`    CreatedAt         time.Time `json:"created_at"`}

So to make this struct reusable, Im gonna change its name to just userResponse. It will be the type of the User field in this loginUserResponse struct:

type userResponse struct {    Username          string    `json:"username"`    FullName          string    `json:"full_name"`    Email             string    `json:"email"`    PasswordChangedAt time.Time `json:"password_changed_at"`    CreatedAt         time.Time `json:"created_at"`}type loginUserResponse struct {    AccessToken string       `json:"access_token"`    User        userResponse `json:"user"`}

Then lets copy the userResponse object from the createUser() handler, and define a newUserResponse() function at the top.

func newUserResponse(user db.User) userResponse {    return userResponse{        Username:          user.Username,        FullName:          user.FullName,        Email:             user.Email,        PasswordChangedAt: user.PasswordChangedAt,        CreatedAt:         user.CreatedAt,    }}

The role of this function is to convert the input db.User object into userResponse. The reason we do that is because theres a sensitive data inside the db.User struct, which is the hashed_password, that we dont want to expose to the client.

OK, so now in the createUser() handler, we can just call the newUserResponse() function to create the response object.

func (server *Server) createUser(ctx *gin.Context) {    ...    user, err := server.store.CreateUser(ctx, arg)    ...    rsp := newUserResponse(user)    ctx.JSON(http.StatusOK, rsp)}

The newUserResponse() function will be useful for our new loginUser() handler as well.

Alright, now lets add a new method to the server struct: loginUser(). Similar as in other API handlers, this function will take a gin.Context object as input.

Inside, we declare a request object of type loginUserRequest, and we call the ctx.ShouldBindJSON() function with a pointer to that request object. This will bind all the input parameters of the API into the request object.

func (server *Server) loginUser(ctx *gin.Context) {    var req loginUserRequest    if err := ctx.ShouldBindJSON(&req); err != nil {        ctx.JSON(http.StatusBadRequest, errorResponse(err))        return    }    ...}

If error is not nil, we send a response with status 400 Bad Request to the client, together with the errorResponse() body to explain why it failed.

If theres no error, we will find the user from the database by calling server.store.GetUser() with the context ctx and req.Username.

func (server *Server) loginUser(ctx *gin.Context) {    ...    user, err := server.store.GetUser(ctx, req.Username)    if err != nil {        if err == sql.ErrNoRows {            ctx.JSON(http.StatusNotFound, errorResponse(err))            return        }        ctx.JSON(http.StatusInternalServerError, errorResponse(err))        return    }    ...}

If the error returned by this call is not nil, then there are 2 possible cases:

  • The first case is when the username doesnt exist, which means error equals to sql.ErrNoRows. In this case, we send a response with status 404 Not Found to the client, and return immediately.
  • The second case is an unexpected error occurs when talking to the database. In this case, we send a 500 Internal Server Error status to the client, and also return right away.

If everything goes well, and no errors occur, we will have to check if the password provided by the client is correct or not. So we call util.CheckPassword() with the input req.Password and user.HashedPassword.

func (server *Server) loginUser(ctx *gin.Context) {    ...    err = util.CheckPassword(req.Password, user.HashedPassword)    if err != nil {        ctx.JSON(http.StatusUnauthorized, errorResponse(err))        return    }    ...}

If this function returns a not nil error, then it means the provided password is incorrect. We will send a response with status 401 Unauthorized to the client, and return.

Only when the password is correct, then we will create a new access token for this user.

Lets call server.tokenMaker.CreateToken(), pass in user.Username, and server.config.AccessTokenDuration as input arguments.

func (server *Server) loginUser(ctx *gin.Context) {    ...    accessToken, err := server.tokenMaker.CreateToken(        user.Username,        server.config.AccessTokenDuration,    )    if err != nil {        ctx.JSON(http.StatusInternalServerError, errorResponse(err))        return    }    rsp := loginUserResponse{        AccessToken: accessToken,        User:        newUserResponse(user),    }    ctx.JSON(http.StatusOK, rsp)}

If an unexpected error occurs, we just return 500 Internal Server Error status code.

Otherwise, we will build the loginUserResponse object, where AccessToken is the created access token, and User is newUserResponse(user). We then send this response to the client with a 200 OK status code.

And thats basically it! The loginUser() handler function is completed:

func (server *Server) loginUser(ctx *gin.Context) {    var req loginUserRequest    if err := ctx.ShouldBindJSON(&req); err != nil {        ctx.JSON(http.StatusBadRequest, errorResponse(err))        return    }    user, err := server.store.GetUser(ctx, req.Username)    if err != nil {        if err == sql.ErrNoRows {            ctx.JSON(http.StatusNotFound, errorResponse(err))            return        }        ctx.JSON(http.StatusInternalServerError, errorResponse(err))        return    }    err = util.CheckPassword(req.Password, user.HashedPassword)    if err != nil {        ctx.JSON(http.StatusUnauthorized, errorResponse(err))        return    }    accessToken, err := server.tokenMaker.CreateToken(        user.Username,        server.config.AccessTokenDuration,    )    if err != nil {        ctx.JSON(http.StatusInternalServerError, errorResponse(err))        return    }    rsp := loginUserResponse{        AccessToken: accessToken,        User:        newUserResponse(user),    }    ctx.JSON(http.StatusOK, rsp)}

Add login API route to the server

The next step is to add a new API endpoint to the server that will route the login request to the loginUser() handler.

Im gonna put it next to the create user route. So router.POST(), the path should be /users/login, and the handler function is server.loginUser().

func NewServer(config util.Config, store db.Store) (*Server, error) {    ...    router := gin.Default()    router.POST("/users", server.createUser)    router.POST("/users/login", server.loginUser)    ...}

And were done!

However, this NewServer() function is getting quite long now, which makes it harder to read.

So Im gonna split the routing part into a separate method of the server struct. Lets call it setupRouter(). Then paste in all the routing codes.

func (server *Server) setupRouter() {    router := gin.Default()    router.POST("/users", server.createUser)    router.POST("/users/login", server.loginUser)    router.POST("/accounts", server.createAccount)    router.GET("/accounts/:id", server.getAccount)    router.GET("/accounts", server.listAccounts)    router.POST("/transfers", server.createTransfer)    server.router = router}

We should move the gin router variable here as well. And at the end, we should save this router to the server.router field.

Then all we have to do in the NewServer() function is to call server.setupRouter().

func NewServer(config util.Config, store db.Store) (*Server, error) {    tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey)    if err != nil {        return nil, fmt.Errorf("cannot create token maker: %w", err)    }    server := &Server{        config:     config,        store:      store,        tokenMaker: tokenMaker,    }    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {        v.RegisterValidation("currency", validCurrency)    }    server.setupRouter()    return server, nil}

And now weve really completed the login user APIs implementation. Its pretty easy and straightforward, isnt it?

Run the server and send login user request

Lets run the server and send some requests to see how it goes!

 make servergo run main.go[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env:   export GIN_MODE=release - using code:  gin.SetMode(gin.ReleaseMode)[GIN-debug] POST   /users                    --> github.com/techschool/simplebank/api.(*Server).createUser-fm (3 handlers)[GIN-debug] POST   /users/login              --> github.com/techschool/simplebank/api.(*Server).loginUser-fm (3 handlers)[GIN-debug] POST   /accounts                 --> github.com/techschool/simplebank/api.(*Server).createAccount-fm (4 handlers)[GIN-debug] GET    /accounts/:id             --> github.com/techschool/simplebank/api.(*Server).getAccount-fm (4 handlers)[GIN-debug] GET    /accounts                 --> github.com/techschool/simplebank/api.(*Server).listAccounts-fm (4 handlers)[GIN-debug] POST   /transfers                --> github.com/techschool/simplebank/api.(*Server).createTransfer-fm (4 handlers)[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080

As you can see here, the login user API is up and running.

Now Im gonna open Postman, create a new request and set it method to POST. The URL should be http://localhost:8080/users/login, then select body, raw, and JSON format.

The JSON body will have 2 fields: username and password. In the database, there are 4 users that we already created in previous lectures. So Im gonna use the first user with username quang1 and the password is secret.

OK lets send this request:

Alt Text

Voil! It's successful!

Weve got the PASETO v2 local access token here. And all the information of the logged in user in this object. So it worked!

Lets try login with an invalid password: xyz. Send the request.

Alt Text

Now weve got 400 Bad Request because the password we sent was too short. That's because we have a validation rule for the password field to have at least 6 characters:

type loginUserRequest struct {    ...    Password string `json:"password" binding:"required,min=6"`}

So lets change this value to xyz123. And send the request again.

Alt Text

This time weve got 401 Unauthorized status code, and the error is: "hashed password is not the hash of the given password", or in other words, the provided password is incorrect.

Now lets try the case when username doesnt exist. Im gonna change the username to quang10, and send the request again.

Alt Text

This time, weve got 404 Not Found status code. Thats exactly what we expected! So the login user API is working very well.

Before we finish, Im gonna show you how easy it is to change the token types.

Change the token type

Right now, were using PASETO, but since it implements the same token maker interface with JWT, it will be super easy if we want to switch to JWT.

All we have to do is just change the token.NewPasetoMaker() call to token.NewJWTMaker() in the api/server.go file:

func NewServer(config util.Config, store db.Store) (*Server, error) {    tokenMaker, err := token.NewJWTMaker(config.TokenSymmetricKey)    if err != nil {        return nil, fmt.Errorf("cannot create token maker: %w", err)    }    ...}

Thats it! Lets restart the server, then go back to Postman and send the login request one more time.

Alt Text

As you can see, the request is successful. And now the access token looks different because its a JWT token, not a PASETO token as before.

OK, now as weve confirmed that it worked, Im gonna revert the token type to PASETO because its better than JWT in my opinion.

func NewServer(config util.Config, store db.Store) (*Server, error) {    tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey)    if err != nil {        return nil, fmt.Errorf("cannot create token maker: %w", err)    }    ...}

And that wraps up this lecture about implementing login user API in Go.

I hope you find it useful. Thanks a lot for reading, and see you soon in the next one!

If you like the article, please subscribe to our Youtube channel and follow us on Twitter or Facebook for more tutorials in the future.


Original Link: https://dev.to/techschoolguru/implement-login-user-api-that-returns-paseto-or-jwt-access-token-in-go-5b1p

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To