JWT authentication
Tags: go
Published: 2021-06-24 17:40:09

Authentication is one of the most important part of almost every web application. We must ensure that every user can create, read, update and delete only data for which it's authorized. For that purpose we will use JWT (JSON Web Token). Fortunately, there are various Golang modules specialized for this. One that will be used in this guide can be found in [this GitHub repo](https://github.com/cristalhq/jwt). Current latest version is v3 which can ne installed by running `go get github.com/cristalhq/jwt/v3`.

Since we will need secret key for generating and verifying tokens, let's add line `export RGB_JWT_SECRET=jwtSecret123` to our `.env` file. Of course, in production you would want to use some long randomly generated string.
Next thing we should do is add new variable to `internal/conf/conf.go`. We will add constant `jwtSecretKey = "RGB_JWT_SECRET"` with the rest of our constants and then add new field `JwtSecret` of type string to `Config` struct. Now we can read new env variable and add it inside of `NewConfig()` function:

```go
const (
  hostKey       = "RGB_HOST"
  portKey       = "RGB_PORT"
  dbHostKey     = "RGB_DB_HOST"
  dbPortKey     = "RGB_DB_PORT"
  dbNameKey     = "RGB_DB_NAME"
  dbUserKey     = "RGB_DB_USER"
  dbPasswordKey = "RGB_DB_PASSWORD"
  jwtSecretKey  = "RGB_JWT_SECRET"
)

type Config struct {
  Host       string
  Port       string
  DbHost     string
  DbPort     string
  DbName     string
  DbUser     string
  DbPassword string
  JwtSecret  string
}

func NewConfig() Config {
  host, ok := os.LookupEnv(hostKey)
  if !ok || host == "" {
    logAndPanic(hostKey)
  }

  port, ok := os.LookupEnv(portKey)
  if !ok || port == "" {
    if _, err := strconv.Atoi(port); err != nil {
      logAndPanic(portKey)
    }
  }

  dbHost, ok := os.LookupEnv(dbHostKey)
  if !ok || dbHost == "" {
    logAndPanic(dbHostKey)
  }

  dbPort, ok := os.LookupEnv(dbPortKey)
  if !ok || dbPort == "" {
    if _, err := strconv.Atoi(dbPort); err != nil {
      logAndPanic(dbPortKey)
    }
  }

  dbName, ok := os.LookupEnv(dbNameKey)
  if !ok || dbName == "" {
    logAndPanic(dbNameKey)
  }

  dbUser, ok := os.LookupEnv(dbUserKey)
  if !ok || dbUser == "" {
    logAndPanic(dbUserKey)
  }

  dbPassword, ok := os.LookupEnv(dbPasswordKey)
  if !ok || dbPassword == "" {
    logAndPanic(dbPasswordKey)
  }

  jwtSecret, ok := os.LookupEnv(jwtSecretKey)
  if !ok || jwtSecret == "" {
    logAndPanic(jwtSecretKey)
  }

  return Config{
    Host:       host,
    Port:       port,
    DbHost:     dbHost,
    DbPort:     dbPort,
    DbName:     dbName,
    DbUser:     dbUser,
    DbPassword: dbPassword,
    JwtSecret:  jwtSecret,
  }
}
```

We will create new file `internal/server/jwt.go`:

```go
package server

import (
  "rgb/internal/conf"

  "github.com/cristalhq/jwt/v3"
  "github.com/rs/zerolog/log"
)

var (
  jwtSigner   jwt.Signer
  jwtVerifier jwt.Verifier
)

func jwtSetup(conf conf.Config) {
  var err error
  key := []byte(conf.JwtSecret)

  jwtSigner, err = jwt.NewSignerHS(jwt.HS256, key)
  if err != nil {
    log.Panic().Err(err).Msg("Error creating JWT signer")
  }

  jwtVerifier, err = jwt.NewVerifierHS(jwt.HS256, key)
  if err != nil {
    log.Panic().Err(err).Msg("Error creating JWT verifier")
  }
}
```

Function `jwtSetup()` will only create signer and verifier that will later be used in authentication. Now we can call this function from `internal/server/server/go` when starting server:

```go
package server

import (
  "rgb/internal/conf"
  "rgb/internal/database"
  "rgb/internal/store"
)

func Start(cfg conf.Config) {
  jwtSetup(cfg)

  store.SetDBConnection(database.NewDBOptions(cfg))

  router := setRouter()

  // Start listening and serving requests
  router.Run(":8080")
}
```

To generate tokens, we will create function in `internal/server/jwt.go`:

```go
func generateJWT(user *store.User) string {
  claims := &jwt.RegisteredClaims{
    ID:        fmt.Sprint(user.ID),
    ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)),
  }
  builder := jwt.NewBuilder(jwtSigner)
  token, err := builder.Build(claims)
  if err != nil {
    log.Panic().Err(err).Msg("Error building JWT")
  }
  return token.String()
}
```

And then we will call it from `internal/server/user.go` instead of hardcoded string we had so far for testing purposes:

```go
package server

import (
  "net/http"
  "rgb/internal/store"

  "github.com/gin-gonic/gin"
)

func signUp(ctx *gin.Context) {
  user := new(store.User)
  if err := ctx.Bind(user); err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  if err := store.AddUser(user); err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, gin.H{
    "msg": "Signed up successfully.",
    "jwt": generateJWT(user),
  })
}

func signIn(ctx *gin.Context) {
  user := new(store.User)
  if err := ctx.Bind(user); err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  user, err := store.Authenticate(user.Username, user.Password)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Sign in failed."})
    return
  }

  ctx.JSON(http.StatusOK, gin.H{
    "msg": "Signed in successfully.",
    "jwt": generateJWT(user),
  })
}
```

Let's test this by signing up or signing in through our frontend. Open browser dev tools and check signIn or signUp response. You can see that our backend now generated random JWT:

![JWT authentication](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w7gjab9qiu8aol6dl7o.png)

Token is now created in signIn and signUp handlers, which means we can verify it for all secured routes. For that we will first implement `verifyJWT()` function in `internal/server/jwt.go`. This function will receive token in the form of string, verify its signature, extract ID from claims and if everything is ok, user's ID will be returned as int:

```go
func verifyJWT(tokenStr string) (int, error) {
  token, err := jwt.Parse([]byte(tokenStr))
  if err != nil {
    log.Error().Err(err).Str("tokenStr", tokenStr).Msg("Error parsing JWT")
    return 0, err
  }

  if err := jwtVerifier.Verify(token.Payload(), token.Signature()); err != nil {
    log.Error().Err(err).Msg("Error verifying token")
    return 0, err
  }

  var claims jwt.StandardClaims
  if err := json.Unmarshal(token.RawClaims(), &claims); err != nil {
    log.Error().Err(err).Msg("Error unmarshalling JWT claims")
    return 0, err
  }

  if notExpired := claims.IsValidAt(time.Now()); !notExpired {
    return 0, errors.New("Token expired.")
  }

  id, err := strconv.Atoi(claims.ID)
  if err != nil {
    log.Error().Err(err).Str("claims.ID", claims.ID).Msg("Error converting claims ID to number")
    return 0, errors.New("ID in token is not valid")
  }
  return id, err
}
```

Functions for generating and verifying are done, and with that we are almost ready to write Gin middleware for authorization. Before that, we will add function that will fetch user from database based on its ID. In `internal/store/users.go`, add function:

```go
func FetchUser(id int) (*User, error) {
  user := new(User)
  user.ID = id
  err := db.Model(user).Returning("*").WherePK().Select()
  if err != nil {
    log.Error().Err(err).Msg("Error fetching user")
    return nil, err
  }
  return user, nil
}
```

It's time to create new file `internal/server/middleware.go`:

```go
package server

import (
  "net/http"
  "rgb/internal/store"
  "strings"

  "github.com/gin-gonic/gin"
)

func authorization(ctx *gin.Context) {
  authHeader := ctx.GetHeader("Authorization")
  if authHeader == "" {
    ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing."})
    return
  }
  headerParts := strings.Split(authHeader, " ")
  if len(headerParts) != 2 {
    ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format is not valid."})
    return
  }
  if headerParts[0] != "Bearer" {
    ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is missing bearer part."})
    return
  }
  userID, err := verifyJWT(headerParts[1])
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
    return
  }
  user, err := store.FetchUser(userID)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
    return
  }
  ctx.Set("user", user)
  ctx.Next()
}
```

Authorization middleware extracts token from `Authorization` header. It first checks if header exists, if it's in valid format, and then calls `verifyJWT()` function. If JWT verification passes, user's ID is returned. User with that ID is fetched from database and set as current user for this context.

Getting current user from context is something that we will need fairly often, so let's extract this into helper function:

```go
func currentUser(ctx *gin.Context) (*store.User, error) {
  var err error
  _user, exists := ctx.Get("user")
  if !exists {
    err = errors.New("Current context user not set")
    log.Error().Err(err).Msg("")
    return nil, err
  }
  user, ok := _user.(*store.User)
  if !ok {
    err = errors.New("Context user is not valid type")
    log.Error().Err(err).Msg("")
    return nil, err
  }
  return user, nil
}
```

First we check if user is set for this context. If not, error is returned. Since `ctx.Get()` returns interface, we must check if value is of type `*store.User`. If not, error is returned. When both checks are passed, current user is returned from context.

Authorization middleware is now ready to use for protected routes, as we will see in next chapter.