go - securing rest api with jwts tokens

JWTs are heaviliy used in single page application as a means to securing an application. there are two main purposes for jwts

  • authorization
  • information exchange

How do they work?

These jwts are signed via passwords or keypairs. jwts are tamper proof because they are signed based on the header and payload

In this turoial we are going to create a client and a server. the client will call an endpoint on the server which is protected by jwts middleware. this middlewear is going to take the token from the header of our request and check if this token is valid based on the request.

creating a simple client

mkdir client
cd client
go mod init github.com/loeken/go-jwts-tutorial/client                                                                            0.00   10:53  
go: creating new go.mod: module github.com/loeken/go-jwts-tutorial/client

The Client

main.go

package main

import("fmt")

func main() {
    fmt.Println("my simple client")
}
test if it works
go run main.go
my simple client

adding jwt to the mix

main.go

package main

import(
    "fmt"
    "time"
    jwt "github.com/dgrijalva/jwt-go"
)
//we can alternatively read the signing key from an environment variable which is better as we dont commit any passwords to our repos
//var mySigningKey = os.Get("MY_JWT_TOKEN)
var mySigningKey = []byte("topsecurephrasecomeshere")
func GenerateJWT() (string, error) {
    token := jwt.New(jwt.SigningMethodHS256)

    claims := token.Claims.(jwt.MapClaims)
    claims["authorized"] = true
    claims["user"] = "loeken"
    claims["exp"] = time.Now().Add(time.Minute * 30).Unix()

    tokenString, err := token.SignedString(mySigningKey)

    if err != nil {
        fmt.Errorf("Something went wrong: %s", err.Error())
        return "", err
    }
    return tokenString, nil
}
func main() {
    fmt.Println("my simple client")
    tokenString, err := GenerateJWT()
    if err != nil {
        fmt.Println("error generating token string")
    }
    fmt.Println(tokenString)
}

next step is turn it into a server that binds to port 9001

main.go

package main

import(
    "fmt"
    "time"
    "log"
    "net/http"
    jwt "github.com/dgrijalva/jwt-go"
)
//we can alternatively read the signing key from an environment variable which is better as we dont commit any passwords to our repos
//var mySigningKey = os.Get("MY_JWT_TOKEN)
var mySigningKey = []byte("topsecurephrasecomeshere")
func GenerateJWT() (string, error) {
    token := jwt.New(jwt.SigningMethodHS256)

    claims := token.Claims.(jwt.MapClaims)
    claims["authorized"] = true
    claims["user"] = "loeken"
    claims["exp"] = time.Now().Add(time.Minute * 30).Unix()

    tokenString, err := token.SignedString(mySigningKey)

    if err != nil {
        fmt.Errorf("Something went wrong: %s", err.Error())
        return "", err
    }
    return tokenString, nil
}

func homePage(w http.ResponseWriter, r *http.Request) {
    validToken, err := GenerateJWT()
    if err != nil {
        fmt.Fprintf(w, err.Error())
    }
    fmt.Fprintf(w, validToken)
}
func handleRequests() {
    http.HandleFunc("/", homePage)
    log.Fatal(http.ListenAndServe(":9001", nil))
}
func main() {
    fmt.Println("my simple client")
    handleRequests()
}

now let’s verify the application is running:

curl localhost:9001
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2MDQyMjc2NTAsInVzZXIiOiJsb2VrZW4ifQ.LEp9Qw0RGM7Z2YU1pxVK59hmeW4ZCuSmS6_hUJTvEFs%  

we get the token so everything seems to be working fine

The Server

So we’re starting out with a very simple Server that only returns the our Super Secret Information ( which we ll restrict access to later on ) but for now we just want a very simple response. this time we ll bind to port 9000

cd ..
mkdir server
cd server
go mod init github.com/loeken/go-jwts-tutorial/server

main.go

package main


import (
    "fmt"
    "log"
    "net/http"
)

func homePage(w http.ResponseWriter, r *http.Request ) {
    fmt.Fprintf(w, "Super Secret Information")
}

func handleRequests() {
    http.HandleFunc("/", homePage)
    log.Fatal(http.ListenAndServe(":9000", nil))
}

func main() {
    fmt.Println("My Simple Server")
    handleRequests()
}
test it
go run ./main.go
My Simple Server
curl localhost:9000 
Super Secret Information

bringing it all together:

main.go

package main


import (
    "fmt"
    "log"
    "net/http"
    jwt "github.com/dgrijalva/jwt-go"
)

func homePage(w http.ResponseWriter, r *http.Request ) {
    fmt.Fprintf(w, "Super Secret Information")
}

//middleware function
var mySigningKey = []byte("topsecurephrasecomeshere")

func isAuthorized(endpoint func(http.ResponseWriter, *http.Request)) http.Handler  { 
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // check if token is set
        if r.Header["Token"] != nil {
            // if it is it starts to parse the token
            token, err := jwt.Parse(r.Header["Token"][0], func(token *jwt.Token) (interface{}, error) {
                if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                    return nil, fmt.Errorf("There was an error")
                }
                return mySigningKey, nil
            })
            // check for errors
            if err != nil {
                fmt.Fprintf(w, err.Error())
            }
            //check if token is valid

            if token.Valid {
                endpoint(w, r)
            }


        } else {
            fmt.Fprintf(w, "Not Authorized")
        }
    })
}
func handleRequests() {
   - http.Handle("/", isAuthorized(homePage))
   +  http.Handle("/", isAuthorized(homePage))
   +  log.Fatal(http.ListenAndServe(":9000", nil))
}
func main() {
    fmt.Println("My Simple Server")
    handleRequests()
}
test it
//generate a token by quering the client
curl localhost:9001
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2MDQyMjkxNzUsInVzZXIiOiJsb2VrZW4ifQ.3XKb5UPsmEBdDHNtKqG8WBjldm6dQ3a2GpeUskg__IU

//use this token to query the server
curl -H "Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2MDQyMjkxNzUsInVzZXIiOiJsb2VrZW4ifQ.3XKb5UPsmEBdDHNtKqG8WBjldm6dQ3a2GpeUskg__IU" localhost:9000
Super Secret Information