DEV Community

Cover image for Building a Web App With Go, Gin and React
Francis Sunday
Francis Sunday

Posted on

Golang Gin Building a Web App With Go, Gin and React

This article was originally posted on My Blog

TL;DR: In this tutorial, I'll show you how easy it is to build a web application with Go and the Gin framework and add authentication to it. Check out the Github repo for the code we're going to write.


Gin is a high-performance micro-framework that delivers a very minimalistic framework that carries with it only the most essential features, libraries, and functionalities needed to build web applications and microservices. It makes it simple to build a request handling pipeline from modular, reusable pieces. It does this by allowing you to write middleware that can be plugged into one or more request handlers or groups of request handlers.

Gin Features

Gin is a fast, simple yet fully featured and very efficient web framework for Go. Check out some of the features below that makes it a worthy framework to consider for your next Golang project.

  • Speed: Gin is built for speed. The framework offers a Radix tree based routing, small memory foot print. No reflection. Predictable API performance.
  • Crash-Free: Gin has the capability of catching crashes or panics during runtime, and can recover from it, this way your application will be always available.
  • Routing: Gin provides a routing interface to allow you express how your web application or API routes should look.
  • JSON Validation: Gin can parse and validate the JSON requests easily, checking for the existence of required values.
  • Error Management: Gin provides a convenient way to collect all the errors occurred during a HTTP request. Eventually, a middleware can write them to a log file, to a database and send them through the network.
  • Built-In Rendering: Gin provides an easy to use API for JSON, XML, and HTML rendering.

Prerequisites

To follow along with this tutorial, you'll need to have Go installed on your machine, a web browser to view the app, and a command line to execute build commands.

Go or as its normally called; "Golang", is a programming language developed by Google for building modern software. Go is a language designed to get stuff done efficiently and fast. The key benefits of Go include:

  • Strongly typed and garbage collected
  • Blazing fast compile times
  • Concurrency built in
  • Extensive standard library

Head over to the downloads section of the Go website, to get Go running on your machine.

Building An App With Gin

We'll be building a simple joke listing app with Gin. Our app will simply list some silly dad jokes. We are going to add authentication to it, all logged-in users will have the privilege to like and view jokes.

This will allow us illustrate how Gin can be used to develop web applications, and/or APIs.

golang-gin-demo-shot

We'll be making use of the following functionalities offered by Gin:

  • Middleware
  • Routing
  • Routes Grouping

Ready, Set, Go

We will write our entire Go application in a main.go file. Since its a small application, its going to be easy to build the application with just go run from the terminal.

We'll create a new directory golang-gin in our Go workspace, and then a main.go file in it:

$ mkdir -p $GOPATH/src/github.com/user/golang-gin
$ cd $GOPATH/src/github.com/user/golang-gin
$ touch main.go
Enter fullscreen mode Exit fullscreen mode

The content of the main.go file:

package main

import (
  "net/http"

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

func main() {
  // Set the router as the default one shipped with Gin
  router := gin.Default()

  // Serve frontend static files
  router.Use(static.Serve("/", static.LocalFile("./views", true)))

  // Setup route group for the API
  api := router.Group("/api")
  {
    api.GET("/", func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H {
        "message": "pong",
      })
    })
  }

  // Start and run the server
  router.Run(":3000")
}
Enter fullscreen mode Exit fullscreen mode

We'll need to create some more directories for our static files. In the same directory as the main.go file, let's create a views folder. In the views folder, create a js folder and an index.html file in it.

The index.html file will be very simple for now:

<!DOCTYPE html>
<html>
<head>
  <title>Jokeish App</title>
</head>

<body>
  <h1>Welcome to the Jokeish App</h1>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Before we test what we have to far, let's install the added dependencies:

$ go get -u github.com/gin-gonic/gin
$ go get -u github.com/gin-gonic/contrib/static
Enter fullscreen mode Exit fullscreen mode

To see what's working, we'll need to start our server by running go run main.go.

go-gin-gorun-1

Once the application is running, navigate to http://localhost:3000 in your browser. If all went well, you should see level 1 header text Welcome to the Jokeish App displayed.

golang-welcome-de

Defining The API

Let's add some more code in our main.go file, for our API definitions. We'll update our main function with two routes /jokes/ and /jokes/like/:jokeID, to the route group /api/.

func main() {
  // ... leave the code above untouched...

  // Our API will consit of just two routes
  // /jokes - which will retrieve a list of jokes a user can see
  // /jokes/like/:jokeID - which will capture likes sent to a particular joke
  api.GET("/jokes", JokeHandler)
  api.POST("/jokes/like/:jokeID", LikeJoke)
}

// JokeHandler retrieves a list of available jokes
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"Jokes handler not implemented yet",
  })
}

// LikeJoke increments the likes of a particular joke Item
func LikeJoke(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"LikeJoke handler not implemented yet",
  })
}
Enter fullscreen mode Exit fullscreen mode

The content of the main.go file, should look like this:

package main

import (
  "net/http"

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

func main() {
  // Set the router as the default one shipped with Gin
  router := gin.Default()

  // Serve frontend static files
  router.Use(static.Serve("/", static.LocalFile("./views", true)))

  // Setup route group for the API
  api := router.Group("/api")
  {
    api.GET("/", func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H {
        "message": "pong",
      })
    })
  }
  // Our API will consit of just two routes
  // /jokes - which will retrieve a list of jokes a user can see
  // /jokes/like/:jokeID - which will capture likes sent to a particular joke
  api.GET("/jokes", JokeHandler)
  api.POST("/jokes/like/:jokeID", LikeJoke)

  // Start and run the server
  router.Run(":3000")
}

// JokeHandler retrieves a list of available jokes
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"Jokes handler not implemented yet",
  })
}

// LikeJoke increments the likes of a particular joke Item
func LikeJoke(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"LikeJoke handler not implemented yet",
  })
}
Enter fullscreen mode Exit fullscreen mode

Let's run our app again go run main.go, and access our routes; http://localhost:3000/api/jokes will return a 200 OK header response, with message jokes handler not implemented yet, and a POST request to http://localhost:3000/api/jokes/like/1 returns a 200 OK header, and message Likejoke handler not implemented yet.

Jokes Data

Since we already have our routes definition set, which does only one thing, which is to return a json response, we'll spice our codebase a bit by adding some more code to it.

// ... leave the code above untouched...

// Let's create our Jokes struct. This will contain information about a Joke

// Joke contains information about a single Joke
type Joke struct {
  ID     int     `json:"id" binding:"required"`
  Likes  int     `json:"likes"`
  Joke   string  `json:"joke" binding:"required"`
}

// We'll create a list of jokes
var jokes = []Joke{
  Joke{1, 0, "Did you hear about the restaurant on the moon? Great food, no atmosphere."},
  Joke{2, 0, "What do you call a fake noodle? An Impasta."},
  Joke{3, 0, "How many apples grow on a tree? All of them."},
  Joke{4, 0, "Want to hear a joke about paper? Nevermind it's tearable."},
  Joke{5, 0, "I just watched a program about beavers. It was the best dam program I've ever seen."},
  Joke{6, 0, "Why did the coffee file a police report? It got mugged."},
  Joke{7, 0, "How does a penguin build it's house? Igloos it together."},
}

func main() {
  // ... leave this block untouched...
}

// JokeHandler retrieves a list of available jokes
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, jokes)
}

// LikeJoke increments the likes of a particular joke Item
func LikeJoke(c *gin.Context) {
  // confirm Joke ID sent is valid
  // remember to import the `strconv` package
  if jokeid, err := strconv.Atoi(c.Param("jokeID")); err == nil {
    // find joke, and increment likes
    for i := 0; i < len(jokes); i++ {
      if jokes[i].ID == jokeid {
        jokes[i].Likes += 1
      }
    }

    // return a pointer to the updated jokes list
    c.JSON(http.StatusOK, &jokes)
  } else {
    // Joke ID is invalid
    c.AbortWithStatus(http.StatusNotFound)
  }
}

// NB: Replace the JokeHandler and LikeJoke functions in the previous version to the ones above
Enter fullscreen mode Exit fullscreen mode

With our code looking good, lets go ahead and test our API. We can test with cURL or postman , and sending a GET request to http://localhost:3000/jokes to get the full list of jokes, and a POST request to http://localhost:3000/jokes/like/{jokeid} to increment the likes of a joke.

$ curl http://localhost:3000/api/jokes

$ curl -X POST http://localhost:3000/api/jokes/like/4
Enter fullscreen mode Exit fullscreen mode

Building the UI (React)

We have our API in place, so let's build a frontend to present the data from our API. For this, we'll be using React. We won't go too deep into React as it will be out of scope for this tutorial. If you need to learn more about React, checkout the official tutorial. You can implement the UI with any frontend framework you're comfortable with.

Setup

We'll edit the index.html file to add external libraries needed to run React, we'll then need to create anapp.jsx file in the views/js directory, which will contain our React code.

Our index.html file should look like this:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  <title>Jokeish App</title>
  <script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
  <script src="https://cdn.auth0.com/js/auth0/9.0/auth0.min.js"></script>
  <script type="application/javascript" src="https://unpkg.com/react@16.0.0/umd/react.production.min.js"></script>
  <script type="application/javascript" src="https://unpkg.com/react-dom@16.0.0/umd/react-dom.production.min.js"></script>
  <script type="application/javascript" src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
  <script type="text/babel" src="js/app.jsx"></script>
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>
  <div id="app"></div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Building our Components

In React views are broken down into components. We'll need to build some components. An App component as the main entry, that launches the application, a Home component which will face non logged-in users, a LoggedIn component with content only visible by authenticated users, and a Joke component to display a list of jokes. We'll write all these components in the app.jsx file.

The App component

This component bootstraps our entire React app. It decides on which component to show when a user is authenticated or not. We'll start off with just its base, and later update it with more functionality.

class App extends React.Component {
  render() {
    if (this.loggedIn) {
      return (<LoggedIn />);
    } else {
      return (<Home />);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Home component

This component is shown to non logged-in users. And a button which opens a Hosted lock screen (we'll add this functionality later), where they can signup, or login.

class Home extends React.Component {
  render() {
    return (
      <div className="container">
        <div className="col-xs-8 col-xs-offset-2 jumbotron text-center">
          <h1>Jokeish</h1>
          <p>A load of Dad jokes XD</p>
          <p>Sign in to get access </p>
          <a onClick={this.authenticate} className="btn btn-primary btn-lg btn-login btn-block">Sign In</a>
        </div>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

LoggedIn component

This component is displayed when a user is authenticated. It stores in its state an array of jokes which is populated when the component mounts.

class LoggedIn extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      jokes: []
    }
  }

  render() {
    return (
      <div className="container">
        <div className="col-lg-12">
          <br />
          <span className="pull-right"><a onClick={this.logout}>Log out</a></span>
          <h2>Jokeish</h2>
          <p>Let's feed you with some funny Jokes!!!</p>
          <div className="row">
            {this.state.jokes.map(function(joke, i){
              return (<Joke key={i} joke={joke} />);
            })}
          </div>
        </div>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

The Joke component

The Joke component will contain information about each item from the jokes response to be displayed.

class Joke extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      liked: ""
    }
    this.like = this.like.bind(this);
  }

  like() {
    // ... we'll add this block later
  }

  render() {
    return (
      <div className="col-xs-4">
        <div className="panel panel-default">
          <div className="panel-heading">#{this.props.joke.id} <span className="pull-right">{this.state.liked}</span></div>
          <div className="panel-body">
            {this.props.joke.joke}
          </div>
          <div className="panel-footer">
            {this.props.joke.likes} Likes &nbsp;
            <a onClick={this.like} className="btn btn-default">
              <span className="glyphicon glyphicon-thumbs-up"></span>
            </a>
          </div>
        </div>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

We've written our components, now let's tell React where to render the app. We'll add the block of code below to the bottom of our app.jsx file.

ReactDOM.render(<App />, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

Let's restart our Go server go run main.go, and head over to our app's URL http://localhost:3000/. You'd see the Home component is being rendered.

golang-gin-home-component

Securing our Jokes App With Auth0

Disclaimer: This isn't a sponsored content.

Auth0 issues JSON Web Tokens on every login for your users. This means that you can have a solid identity infrastructure, including single sign-on, user management, support for social identity providers (Facebook, Github, Twitter, etc.), enterprise identity providers (Active Directory, LDAP, SAML, etc.) and your own database of users with just a few lines of code.

We can easily set up authentication in our GIN app by using Auth0. You'll need an ccount to follow along with this part. If you don't already have an Auth0 account, sign up for one now.

Creating the API client

Our tokens will be generated with Auth0, so we need to create an API and a Client from our Auth0 dashboard. If you haven't already, sign up for an Auth0 account.

To create a new API, navigate to the APIs section in your dashboard, and click the Create API button.

golang-gin-apis-section

Choose an API name, and an identifier. The identifier will be the audience for the middleware. The Signing Algorithm should be RS256.

To create a new Client, navigate to the clients section in your dashboard, and click the Create Client button, and select type Regular Web Applications.

golang-gin-create-client

Once the client is created, take note of the client_id and client_secret, as we'll need it later.

golang-gin-client-page

We need to add the credentials needed for our API to an environment vairable. In the root directory, create a new file .env and add the following to it, with the details from the Auth0 dashboard:

export AUTH0_API_CLIENT_SECRET=""
export AUTH0_CLIENT_ID=""
export AUTH0_DOMAIN="yourdomain.auth0.com"
export AUTH0_API_AUDIENCE=""
Enter fullscreen mode Exit fullscreen mode

Securing our API Endpoints

Currently, our API is open to the world, so we need to secure them, so only authorized users can access them.

We are going to make use of a JWT Middleware to check for a valid JSON Web Token from each requests hitting our Endpoints.

Let's create our middleware:


// ...

var jwtMiddleWare *jwtmiddleware.JWTMiddleware

func main() {
  jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      aud := os.Getenv("AUTH0_API_AUDIENCE")
      checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
      if !checkAudience {
        return token, errors.New("Invalid audience.")
      }
      // verify iss claim
      iss := os.Getenv("AUTH0_DOMAIN")
      checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
      if !checkIss {
        return token, errors.New("Invalid issuer.")
      }

      cert, err := getPemCert(token)
      if err != nil {
        log.Fatalf("could not get cert: %+v", err)
      }

      result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
      return result, nil
    },
    SigningMethod: jwt.SigningMethodRS256,
  })

  // register our actual jwtMiddleware
  jwtMiddleWare = jwtMiddleware

  // ... the rest of the code below this function doesn't change yet
}

// authMiddleware intercepts the requests, and check for a valid jwt token
func authMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    // Get the client secret key
    err := jwtMiddleWare.CheckJWT(c.Writer, c.Request)
    if err != nil {
      // Token not found
      fmt.Println(err)
      c.Abort()
      c.Writer.WriteHeader(http.StatusUnauthorized)
      c.Writer.Write([]byte("Unauthorized"))
      return
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The above code, we have a new jwtMiddleWare variable which is initialized in the main function, and is used in the authMiddleware middle function. If you notice, we are pulling our server-side credentials from an environment variable (one of the tenets of a 12-factor app). Our middleware checks and receives a token from a request, it calls the jwtMiddleWare.CheckJWT method to validate the token sent.

Let's also write the function to return the JSON Web Keys:

// ... the code above is untouched...

// Jwks stores a slice of JSON Web Keys
type Jwks struct {
  Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
  Kty string   `json:"kty"`
  Kid string   `json:"kid"`
  Use string   `json:"use"`
  N   string   `json:"n"`
  E   string   `json:"e"`
  X5c []string `json:"x5c"`
}

func main() {
  // ... the code in this method is untouched...
}

func getPemCert(token *jwt.Token) (string, error) {
  cert := ""
  resp, err := http.Get(os.Getenv("AUTH0_DOMAIN") + ".well-known/jwks.json")
  if err != nil {
    return cert, err
  }
  defer resp.Body.Close()

  var jwks = Jwks{}
  err = json.NewDecoder(resp.Body).Decode(&jwks)

  if err != nil {
    return cert, err
  }

  x5c := jwks.Keys[0].X5c
  for k, v := range x5c {
    if token.Header["kid"] == jwks.Keys[k].Kid {
      cert = "-----BEGIN CERTIFICATE-----\n" + v + "\n-----END CERTIFICATE-----"
    }
  }

  if cert == "" {
    return cert, errors.New("unable to find appropriate key.")
  }

  return cert, nil
}
Enter fullscreen mode Exit fullscreen mode

Using the JWT Middleware

Using the middleware is very straight forward. We just pass it as a parameter to our routes definition.

...

api.GET("/jokes", authMiddleware(), JokeHandler)
api.POST("/jokes/like/:jokeID", authMiddleware(), LikeJoke)

...
Enter fullscreen mode Exit fullscreen mode

Our main.go file should look like this:

package main

import (
  "encoding/json"
  "errors"
  "fmt"
  "log"
  "net/http"
  "os"
  "strconv"

  jwtmiddleware "github.com/auth0/go-jwt-middleware"
  jwt "github.com/dgrijalva/jwt-go"
  "github.com/gin-gonic/contrib/static"
  "github.com/gin-gonic/gin"
)

type Response struct {
  Message string `json:"message"`
}

type Jwks struct {
  Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
  Kty string   `json:"kty"`
  Kid string   `json:"kid"`
  Use string   `json:"use"`
  N   string   `json:"n"`
  E   string   `json:"e"`
  X5c []string `json:"x5c"`
}

type Joke struct {
  ID    int    `json:"id" binding:"required"`
  Likes int    `json:"likes"`
  Joke  string `json:"joke" binding:"required"`
}

/** we'll create a list of jokes */
var jokes = []Joke{
  Joke{1, 0, "Did you hear about the restaurant on the moon? Great food, no atmosphere."},
  Joke{2, 0, "What do you call a fake noodle? An Impasta."},
  Joke{3, 0, "How many apples grow on a tree? All of them."},
  Joke{4, 0, "Want to hear a joke about paper? Nevermind it's tearable."},
  Joke{5, 0, "I just watched a program about beavers. It was the best dam program I've ever seen."},
  Joke{6, 0, "Why did the coffee file a police report? It got mugged."},
  Joke{7, 0, "How does a penguin build it's house? Igloos it together."},
}

var jwtMiddleWare *jwtmiddleware.JWTMiddleware

func main() {
  jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      aud := os.Getenv("AUTH0_API_AUDIENCE")
      checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
      if !checkAudience {
        return token, errors.New("Invalid audience.")
      }
      // verify iss claim
      iss := os.Getenv("AUTH0_DOMAIN")
      checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
      if !checkIss {
        return token, errors.New("Invalid issuer.")
      }

      cert, err := getPemCert(token)
      if err != nil {
        log.Fatalf("could not get cert: %+v", err)
      }

      result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
      return result, nil
    },
    SigningMethod: jwt.SigningMethodRS256,
  })

  jwtMiddleWare = jwtMiddleware
  // Set the router as the default one shipped with Gin
  router := gin.Default()

  // Serve the frontend
  router.Use(static.Serve("/", static.LocalFile("./views", true)))

  api := router.Group("/api")
  {
    api.GET("/", func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H{
        "message": "pong",
      })
    })
    api.GET("/jokes", authMiddleware(), JokeHandler)
    api.POST("/jokes/like/:jokeID", authMiddleware(), LikeJoke)
  }
  // Start the app
  router.Run(":3000")
}

func getPemCert(token *jwt.Token) (string, error) {
  cert := ""
  resp, err := http.Get(os.Getenv("AUTH0_DOMAIN") + ".well-known/jwks.json")
  if err != nil {
    return cert, err
  }
  defer resp.Body.Close()

  var jwks = Jwks{}
  err = json.NewDecoder(resp.Body).Decode(&jwks)

  if err != nil {
    return cert, err
  }

  x5c := jwks.Keys[0].X5c
  for k, v := range x5c {
    if token.Header["kid"] == jwks.Keys[k].Kid {
      cert = "-----BEGIN CERTIFICATE-----\n" + v + "\n-----END CERTIFICATE-----"
    }
  }

  if cert == "" {
    return cert, errors.New("unable to find appropriate key")
  }

  return cert, nil
}

// authMiddleware intercepts the requests, and check for a valid jwt token
func authMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    // Get the client secret key
    err := jwtMiddleWare.CheckJWT(c.Writer, c.Request)
    if err != nil {
      // Token not found
      fmt.Println(err)
      c.Abort()
      c.Writer.WriteHeader(http.StatusUnauthorized)
      c.Writer.Write([]byte("Unauthorized"))
      return
    }
  }
}

// JokeHandler returns a list of jokes available (in memory)
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")

  c.JSON(http.StatusOK, jokes)
}

func LikeJoke(c *gin.Context) {
  // Check joke ID is valid
  if jokeid, err := strconv.Atoi(c.Param("jokeID")); err == nil {
    // find joke and increment likes
    for i := 0; i < len(jokes); i++ {
      if jokes[i].ID == jokeid {
        jokes[i].Likes = jokes[i].Likes + 1
      }
    }
    c.JSON(http.StatusOK, &jokes)
  } else {
    // the jokes ID is invalid
    c.AbortWithStatus(http.StatusNotFound)
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's install the jwtmiddleware libraries:

$ go get -u github.com/auth0/go-jwt-middleware
$ go get -u github.com/dgrijalva/jwt-go
Enter fullscreen mode Exit fullscreen mode

Let's source our environment file, and restart our app server:

$ source .env
$ go run main.go
Enter fullscreen mode Exit fullscreen mode

Now if we try accessing any of the endpoints, you'd be faced with a 401 Unauthorized error. That's because we need to send along a token with the request.

Login with Auth0 and React

Let's implement a login system, so users can login or create accounts, so they have access to our jokes. We'll add to our app.jsx file, the following Auth0 credentials:

  • AUTH0_CLIENT_ID
  • AUTH0_DOMAIN
  • AUTH0_CALLBACK_URL - The URL of your app
  • AUTH0_API_AUDIENCE

You can find the AUTH0_CLIENT_ID, AUTH0_DOMAIN, and AUTH0_API_AUDIENCE data from your Auth0 management dashboard.

We need to set a callback which Auth0 redirects to. Navigate to the Clients section in your dashboard, and in the settings, let's set the callback to http://localhost:3000:

auth0-golang-gin-allowed-callbacks

With the credentials in place, lets update our React components.

APP component

const AUTH0_CLIENT_ID = "aIAOt9fkMZKrNsSsFqbKj5KTI0ObTDPP";
const AUTH0_DOMAIN = "hakaselabs.auth0.com";
const AUTH0_CALLBACK_URL = location.href;
const AUTH0_API_AUDIENCE = "golang-gin";

class App extends React.Component {
  parseHash() {
    this.auth0 = new auth0.WebAuth({
      domain: AUTH0_DOMAIN,
      clientID: AUTH0_CLIENT_ID
    });
    this.auth0.parseHash(window.location.hash, (err, authResult) => {
      if (err) {
        return console.log(err);
      }
      if (
        authResult !== null &&
        authResult.accessToken !== null &&
        authResult.idToken !== null
      ) {
        localStorage.setItem("access_token", authResult.accessToken);
        localStorage.setItem("id_token", authResult.idToken);
        localStorage.setItem(
          "profile",
          JSON.stringify(authResult.idTokenPayload)
        );
        window.location = window.location.href.substr(
          0,
          window.location.href.indexOf("#")
        );
      }
    });
  }

  setup() {
    $.ajaxSetup({
      beforeSend: (r) => {
        if (localStorage.getItem("access_token")) {
          r.setRequestHeader(
            "Authorization",
            "Bearer " + localStorage.getItem("access_token")
          );
        }
      }
    });
  }

  setState() {
    let idToken = localStorage.getItem("id_token");
    if (idToken) {
      this.loggedIn = true;
    } else {
      this.loggedIn = false;
    }
  }

  componentWillMount() {
    this.setup();
    this.parseHash();
    this.setState();
  }

  render() {
    if (this.loggedIn) {
      return <LoggedIn />;
    }
    return <Home />;
  }
}
Enter fullscreen mode Exit fullscreen mode

We updated the App component with three component methods (setup, parseHash and setState), and a lifecycle method componentWillMount. The parseHash method, initializes the auth0 webAuth client, and parses the hash to a more readable format, saving them to localSt. to show the lock screen, capture and store the user token and add the correct authorization header to any requests to our API

Home component

Our Home component will be updated, we'll add the functionality for the authenticate method, which will trigger the hosted lock screen to display, and allow our users login or signup.

class Home extends React.Component {
  constructor(props) {
    super(props);
    this.authenticate = this.authenticate.bind(this);
  }
  authenticate() {
    this.WebAuth = new auth0.WebAuth({
      domain: AUTH0_DOMAIN,
      clientID: AUTH0_CLIENT_ID,
      scope: "openid profile",
      audience: AUTH0_API_AUDIENCE,
      responseType: "token id_token",
      redirectUri: AUTH0_CALLBACK_URL
    });
    this.WebAuth.authorize();
  }

  render() {
    return (
      <div className="container">
        <div className="row">
          <div className="col-xs-8 col-xs-offset-2 jumbotron text-center">
            <h1>Jokeish</h1>
            <p>A load of Dad jokes XD</p>
            <p>Sign in to get access </p>
            <a
              onClick={this.authenticate}
              className="btn btn-primary btn-lg btn-login btn-block"
            >
              Sign In
            </a>
          </div>
        </div>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

LoggedIn Component

We will update the LoggedIn component to communicate with our API, and pull all jokes, pass each joke as a prop to the Joke component, which renders a bootstrap pannel. Let's write those:

class LoggedIn extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      jokes: []
    };

    this.serverRequest = this.serverRequest.bind(this);
    this.logout = this.logout.bind(this);
  }

  logout() {
    localStorage.removeItem("id_token");
    localStorage.removeItem("access_token");
    localStorage.removeItem("profile");
    location.reload();
  }

  serverRequest() {
    $.get("http://localhost:3000/api/jokes", res => {
      this.setState({
        jokes: res
      });
    });
  }

  componentDidMount() {
    this.serverRequest();
  }

  render() {
    return (
      <div className="container">
        <br />
        <span className="pull-right">
          <a onClick={this.logout}>Log out</a>
        </span>
        <h2>Jokeish</h2>
        <p>Let's feed you with some funny Jokes!!!</p>
        <div className="row">
          <div className="container">
            {this.state.jokes.map(function(joke, i) {
              return <Joke key={i} joke={joke} />;
            })}
          </div>
        </div>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Joke Component

We'll also update the Joke component to format each Joke item passed to it from the Parent compoent (LoggedIn), and add a like method, which will increment the likes of a Joke.

class Joke extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      liked: "",
      jokes: []
    };
    this.like = this.like.bind(this);
    this.serverRequest = this.serverRequest.bind(this);
  }

  like() {
    let joke = this.props.joke;
    this.serverRequest(joke);
  }
  serverRequest(joke) {
    $.post(
      "http://localhost:3000/api/jokes/like/" + joke.id,
      { like: 1 },
      res => {
        console.log("res... ", res);
        this.setState({ liked: "Liked!", jokes: res });
        this.props.jokes = res;
      }
    );
  }

  render() {
    return (
      <div className="col-xs-4">
        <div className="panel panel-default">
          <div className="panel-heading">
            #{this.props.joke.id}{" "}
            <span className="pull-right">{this.state.liked}</span>
          </div>
          <div className="panel-body">{this.props.joke.joke}</div>
          <div className="panel-footer">
            {this.props.joke.likes} Likes &nbsp;
            <a onClick={this.like} className="btn btn-default">
              <span className="glyphicon glyphicon-thumbs-up" />
            </a>
          </div>
        </div>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together

With the UI and API complete, we can test our app. We'll start of by booting our server source .env && go run main.go, and the navigate to http://localhost:3000 from any browser, you should see the Home component with a signin button. Clicking on the signin button will redirect to a hosted Lock page, create an account or login, to continue using the application.

golang-gin-home-component
Home

golang-gin-login-screen
Auth0 Hosted Lock Screen

golang-gin-demo-shot
LoggedIn App view

Conclusion

Congrats! You have learned how to build an application and an API with Go and the GIN framework.

This tutorial is designed to help you get started on building and adding authentication to a Golang app with the GIN framework.

Did I miss something important? Let me know of it in the comments.

Top comments (30)

Collapse
 
agrim profile image
Agrim Prasad • Edited

Many thanks for this great blog post!

We've been using Gin as our web framework in production, and while it holds up very well under high load, we've seen some issues with the router used within the framework. The issue is detailed in github.com/gin-gonic/gin/issues/388

In brief, if you have two routes defined as follows, you'll see a runtime panic :

r.GET("/teachers/list", func (c *gin.Context){})
r.GET("/teachers/:id/profile", func (c *gin.Context){})
Enter fullscreen mode Exit fullscreen mode

This is because gin's built-in router (based on httprouter) can't handle such wildcard based routing properly. As a result, RESTful services are difficult to define around resource names.

I've been exploring alternatives to using gin, and it seems like go-chi is the most simple and idiomatic router around. It could successfully handle such wildcard based routing. Furthermore, it uses the native Go context object (rather than Gin's custom context object), and its middlewares and handlers are portable across other routing libraries such as gorilla/mux (in case you want to change in the future).

Some comparisons between simple Go http routers made by me in this repo

Collapse
 
criszelaya24 profile image
Cristopher Palacios • Edited

I had an issue and is the next:

"Error parsing token: Invalid issuer."

I followed all the steps and I don't have a clue what could be my problem, I made the right configuration on auth0 and I got all the right credencial from it, Any advise?

I new in all these (Go,Gin and React)! and this post is the best that I have found so far!

Thanks for the time you took it to make it!

Collapse
 
codehakase profile image
Francis Sunday

Hello there Christopher! Glad you checked out the article.

The error message, That's coming from the method that validates the token sent from the Auth0 client. Confirm you have the .env file setup and source on runtime with the Go server

Collapse
 
jstnblau profile image
Justin Blau

Hello, I ran into this same issue. The solution on my side was that although on the Auth0 site the domain is listed as 'example.auth0.com' in the .env and app.jsx files it needed to be set to 'example.auth0.com'

Collapse
 
atinmathur profile image
Atin Mathur • Edited

Hi Justin, I am getting the "Error parsing token: invalid issuer". I think I have set the right domain which is "username.us.auth0.com". And I didn't get your solution to this problem. Can you please explain again?

Collapse
 
zxiang77 profile image
zili

Nice post! I successfully setup a simple application with your example without any problem. But now I have a question beyond the scope of this article: I want to setup some React router e.g.: having a /jokes for the page in your example. but gin is unable to comprehend the url /jokes (vs. the previous /) and return a 404. is there a way to setup the route in gin to also respect the routes defined in React router?
Thanks!

Collapse
 
zxiang77 profile image
zili • Edited

Did some research on serving React with static file (create-react-app.dev/docs/deployme...), so basically I have to define the client side routing paths in the go server, and point them to the same static files, e.g.:
router.Use(static.Serve("/jokes", static.LocalFile("./views", true)))

Also because gin doesn't support path regex, I have to either customize a handler for that case or define all my client side routing on the go backend. a bit tedious, but that works! :)
github.com/gin-gonic/gin/issues/229

Collapse
 
jvarness profile image
Jake Varness

Looks quite extensive! Definitely going to need to check this out later. Bookmarked!

Collapse
 
codehakase profile image
Francis Sunday

Great

Collapse
 
jvarness profile image
Jake Varness

For real though, pretty sure this post could replace this book:

Cloud Native Go: Building Web Applications and Microservices for the Cloud with Go and React (Developer's Library) amazon.com/dp/0672337797/ref=cm_sw...

Collapse
 
natashabieber2 profile image
Natasha Bieber

Thank you Francis for providing the detailed information about web application development. The blog was absolutely amazing! Lot of information which can be useful for web app users, Keep it up. bit.ly/2RHSivk

Collapse
 
samrocksc profile image
Sam Clark

This is amazing! Thank you so much Francis!

Collapse
 
codehakase profile image
Francis Sunday

You're welcome Sam! I'm glad you liked it :)

Collapse
 
codehakase profile image
Francis Sunday

Hello Nico!

I'm glad you enjoyed reading this piece of mine. Do try it out, and also explore other libraries available in the Go ecosystem, Good luck :D

Collapse
 
iwilsonq profile image
Ian Wilson

Awesome post, you went into great detail!

Collapse
 
codehakase profile image
Francis Sunday

Thank you Lan, I'm glad you liked it :)

Collapse
 
leo_png profile image
Leo.png

Great, great post ! Thanks

Collapse
 
codehakase profile image
Francis Sunday

Thanks Leo

Collapse
 
crosdev profile image
José Martínez

Thanks for sharing. It's a great work.

Collapse
 
codehakase profile image
Francis Sunday

Thanks, I'm glad you liked it José.

Collapse
 
bajusz15 profile image
Máté Bajusz

Are you sure it's safe to store JWT in localstorage? From my understanding, this is vulnerable to any XSS attack. Apart from that, I liked the tutorial!

Collapse
 
bajusz15 profile image
Máté Bajusz

Are you sure it's safe to store JWT in localstorage? From my understanding this is vulnerable to XSS attacks. Apart from that, I liked the tutorial!

Collapse
 
bikrambhattacharya profile image
Bikram Bhattacharya

Thanks a lot.

Collapse
 
codehakase profile image
Francis Sunday

You're welcome ☺

Collapse
 
noncototient profile image
Bo

Why use external service for auth and not the Go server itself?

Collapse
 
codehakase profile image
Francis Sunday

The article was focused on that particular service integration.

Collapse
 
rafaela314 profile image
Rafaela Cavalcante

I tried to run and got "unknown field 'SigninMethod' in struct literal of type jwtmiddleware.Options". Any clues on how to solve It?