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.
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
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")
}
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>
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
To see what's working, we'll need to start our server by running go run main.go
.
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.
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",
})
}
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",
})
}
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
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
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>
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 />);
}
}
}
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>
)
}
}
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>
)
}
}
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
<a onClick={this.like} className="btn btn-default">
<span className="glyphicon glyphicon-thumbs-up"></span>
</a>
</div>
</div>
</div>
)
}
}
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'));
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.
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.
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
.
Once the client is created, take note of the client_id
and client_secret
, as we'll need it later.
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=""
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
}
}
}
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
}
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)
...
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)
}
}
Let's install the jwtmiddleware
libraries:
$ go get -u github.com/auth0/go-jwt-middleware
$ go get -u github.com/dgrijalva/jwt-go
Let's source our environment file, and restart our app server:
$ source .env
$ go run main.go
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
, andAUTH0_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
:
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 />;
}
}
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>
);
}
}
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>
);
}
}
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
<a onClick={this.like} className="btn btn-default">
<span className="glyphicon glyphicon-thumbs-up" />
</a>
</div>
</div>
</div>
)
}
}
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.
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 (28)
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 :
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
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!
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 serverHello, 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'
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?
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!
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
Looks quite extensive! Definitely going to need to check this out later. Bookmarked!
Great
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...
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
This is amazing! Thank you so much Francis!
You're welcome Sam! I'm glad you liked it :)
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
Awesome post, you went into great detail!
Thank you Lan, I'm glad you liked it :)
Great, great post ! Thanks
Thanks Leo
Thanks for sharing. It's a great work.
Thanks, I'm glad you liked it José.