Securing Golang API using Json Web Token (JWT)

What is JSON Web Token (JWT)

According to the official site (http://jwt.io/):

  1. iss. Is the issuer of the claim. Connect uses it to identify the application making the call.
  2. iat. Issued-at time. Contains the UTC Unix time at which this token was issued. There are no hard requirements around this claim but it does not make sense for it to be significantly in the future. Also, significantly old issued-at times may indicate the replay of suspiciously old tokens.
  3. sub. The subject of this token. This is the user associated with the relevant action, and may not be present if there is no logged in user.

Authentication with JWT

JSON Web Tokens (JWT) are a more modern approach to authentication. As the web moves to a greater separation between the client and server, JWT provides a wonderful alternative to traditional cookie based authentication models.

Token-Based Auth

Benefits of using a token-based approach

  • Cross-domain / CORS: cookies + CORS don’t play well across different domains. A token-based approach allows you to make AJAX calls to any server, on any domain because you use an HTTP header to transmit the user information.
  • Stateless: there is no need to keep a session store, the token is a self-contained entity that conveys all the user information.
  • CDN: you can serve all the assets of your app from a CDN (e.g. javascript, HTML, images, etc.), and your server side is just the API.
  • Decoupling: you are not tied to a particular authentication scheme. The token might be generated anywhere, hence your API can be called from anywhere with a single way of authenticating those calls.
  • Mobile ready: when you start working on a native platform (iOS, Android, Windows 8, etc.) cookies are not ideal when consuming a secure API (you have to deal with cookie containers). Adopting a token-based approach simplifies this a lot.
  • CRSF: since you are not relying on cookies, you don’t need to protect against cross site requests.

Organizing our Application code

Why modular? Having all of our functionality in different modules helps us in many ways:

  1. The overall application layout is easier to understand.
  2. You can see how the parts work together since modules have to be injected before use.
  3. Code is reusable since all of the necessary functionality is contained inside the module.
  4. Testing your code is much easier.

Creating Routers

The code shown below creates a router assigning each route with his controller who runs when that endpoint is called.

package routersimport (
"github.com/gorilla/mux"
)
func InitRoutes() *mux.Router {
router := mux.NewRouter()
router = SetHelloRoutes(router)
router = SetAuthenticationRoutes(router)
return router
}
package routersimport (
"api.jwt.auth/controllers"
"api.jwt.auth/core/authentication"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
)
func SetAuthenticationRoutes(router *mux.Router) *mux.Router {
router.HandleFunc(
"/token-auth",
controllers.Login
).Methods("POST")

router.Handle(
"/refresh-token-auth",
negroni.New(
negroni.HandlerFunc(controllers.RefreshToken),
)).Methods("GET")

router.Handle(
"/logout",
negroni.New(
negroni.HandlerFunc(
authentication.RequireTokenAuthentication
),
negroni.HandlerFunc(controllers.Logout),
)).Methods("GET")
return router
}
package routersimport (
"api.jwt.auth/controllers"
"api.jwt.auth/core/authentication"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
)
func SetHelloRoutes(router *mux.Router) *mux.Router {
router.Handle(
"/test/hello",
negroni.New(
negroni.HandlerFunc(controllers.HelloController),
)).Methods("GET")

return router
}
package controllersimport (
"api.jwt.auth/services"
"api.jwt.auth/services/models"
"encoding/json"
"net/http"
)
func Login(w http.ResponseWriter, r *http.Request) {
requestUser := new(models.User)
decoder := json.NewDecoder(r.Body)
decoder.Decode(&requestUser)
responseStatus, token := services.Login(requestUser)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(responseStatus)
w.Write(token)
}
func RefreshToken(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
requestUser := new(models.User)
decoder := json.NewDecoder(r.Body)
decoder.Decode(&requestUser)
w.Header().Set("Content-Type", "application/json")
w.Write(services.RefreshToken(requestUser))
}
func Logout(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
err := services.Logout(r)
w.Header().Set("Content-Type", "application/json")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
}
}
package controllersimport (
"net/http"
)
func HelloController(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Write([]byte("Hello, World!"))
}
package modelstype User struct {
UUID string `json:"uuid" form:"-"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
package servicesimport (
"api.jwt.auth/api/parameters"
"api.jwt.auth/core/authentication"
"api.jwt.auth/services/models"
"encoding/json"
jwt "github.com/dgrijalva/jwt-go"
"net/http"
)
func Login(requestUser *models.User) (int, []byte) {
authBackend := authentication.InitJWTAuthenticationBackend()
if authBackend.Authenticate(requestUser) {
token, err := authBackend.GenerateToken(requestUser.UUID)
if err != nil {
return http.StatusInternalServerError, []byte("")
} else {
response, _ := json.Marshal(parameters.TokenAuthentication{token})
return http.StatusOK, response
}
}
return http.StatusUnauthorized, []byte("")
}
func RefreshToken(requestUser *models.User) []byte {
authBackend := authentication.InitJWTAuthenticationBackend()
token, err := authBackend.GenerateToken(requestUser.UUID)
if err != nil {
panic(err)
}
response, err := json.Marshal(parameters.TokenAuthentication{token})
if err != nil {
panic(err)
}
return response
}
func Logout(req *http.Request) error {
authBackend := authentication.InitJWTAuthenticationBackend()
tokenRequest, err := jwt.ParseFromRequest(req, func(token *jwt.Token) (interface{}, error) {
return authBackend.PublicKey, nil
})
if err != nil {
return err
}
tokenString := req.Header.Get("Authorization")
return authBackend.Logout(tokenString, tokenRequest)
}
package mainimport (
"api.jwt.auth/routers"
"api.jwt.auth/settings"
"github.com/codegangsta/negroni"
"net/http"
)
func main() {
settings.Init()
router := routers.InitRoutes()
n := negroni.Classic()
n.UseHandler(router)
http.ListenAndServe(":5000", n)
}

Applying authentication with JWT

As we can see right now, our API endpoint ”/test/hello” is insecure. Let’s add our “RequireTokenAuthentication” middleware to protect it.

package authenticationimport (
"fmt"
jwt "github.com/dgrijalva/jwt-go"
"net/http"
)
func RequireTokenAuthentication(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
authBackend := InitJWTAuthenticationBackend()
token, err := jwt.ParseFromRequest(req, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
} else {
return authBackend.PublicKey, nil
}
})
if err == nil && token.Valid && !authBackend.IsInBlacklist(req.Header.Get("Authorization")) {
next(rw, req)
} else {
rw.WriteHeader(http.StatusUnauthorized)
}
}
package routersimport (
"api.jwt.auth/controllers"
"api.jwt.auth/core/authentication"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
)
func SetHelloRoutes(router *mux.Router) *mux.Router {
router.Handle("/test/hello",
negroni.New(
negroni.HandlerFunc(authentication.RequireTokenAuthentication),
negroni.HandlerFunc(controllers.HelloController),
)).Methods("GET")
return router
}

Let’s try our luck again!

If we can try again without a valid token, we’ll receive a unauthorized error response:

Session control & Token Logout

The last step, or bonus step, is how we can manage user logout and invalidate his token.

package authenticationimport (
"api.jwt.auth/core/redis"
"api.jwt.auth/services/models"
"api.jwt.auth/settings"
"bufio"
"code.google.com/p/go-uuid/uuid"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
jwt "github.com/dgrijalva/jwt-go"
"golang.org/x/crypto/bcrypt"
"os"
"time"
)
type JWTAuthenticationBackend struct {
privateKey *rsa.PrivateKey
PublicKey *rsa.PublicKey
}
const (
tokenDuration = 72
expireOffset = 3600
)
var authBackendInstance *JWTAuthenticationBackend = nilfunc InitJWTAuthenticationBackend() *JWTAuthenticationBackend {
if authBackendInstance == nil {
authBackendInstance = &JWTAuthenticationBackend{
privateKey: getPrivateKey(),
PublicKey: getPublicKey(),
}
}

return authBackendInstance
}
func (backend *JWTAuthenticationBackend) GenerateToken(userUUID string) (string, error) {
token := jwt.New(jwt.SigningMethodRS512)
token.Claims["exp"] = time.Now().Add(time.Hour * time.Duration(settings.Get().JWTExpirationDelta)).Unix()
token.Claims["iat"] = time.Now().Unix()
token.Claims["sub"] = userUUID
tokenString, err := token.SignedString(backend.privateKey)
if err != nil {
panic(err)
return "", err
}
return tokenString, nil
}
func (backend *JWTAuthenticationBackend) Authenticate(user *models.User) bool {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("testing"), 10)

testUser := models.User{
UUID: uuid.New(),
Username: "haku",
Password: string(hashedPassword),
}

return user.Username == testUser.Username && bcrypt.CompareHashAndPassword([]byte(testUser.Password), []byte(user.Password)) == nil
}
func (backend *JWTAuthenticationBackend) getTokenRemainingValidity(timestamp interface{}) int {
if validity, ok := timestamp.(float64); ok {
tm := time.Unix(int64(validity), 0)
remainer := tm.Sub(time.Now())
if remainer > 0 {
return int(remainer.Seconds() + expireOffset)
}
}
return expireOffset
}
func (backend *JWTAuthenticationBackend) Logout(tokenString string, token *jwt.Token) error {
redisConn := redis.Connect()
return redisConn.SetValue(tokenString, tokenString, backend.getTokenRemainingValidity(token.Claims["exp"]))
}
func (backend *JWTAuthenticationBackend) IsInBlacklist(token string) bool {
redisConn := redis.Connect()
redisToken, _ := redisConn.GetValue(token)

if redisToken == nil {
return false
}

return true
}
func getPrivateKey() *rsa.PrivateKey {
privateKeyFile, err := os.Open(settings.Get().PrivateKeyPath)
if err != nil {
panic(err)
}

pemfileinfo, _ := privateKeyFile.Stat()
var size int64 = pemfileinfo.Size()
pembytes := make([]byte, size)

buffer := bufio.NewReader(privateKeyFile)
_, err = buffer.Read(pembytes)

data, _ := pem.Decode([]byte(pembytes))

privateKeyFile.Close()

privateKeyImported, err := x509.ParsePKCS1PrivateKey(data.Bytes)

if err != nil {
panic(err)
}

return privateKeyImported
}
func getPublicKey() *rsa.PublicKey {
publicKeyFile, err := os.Open(settings.Get().PublicKeyPath)
if err != nil {
panic(err)
}

pemfileinfo, _ := publicKeyFile.Stat()
var size int64 = pemfileinfo.Size()
pembytes := make([]byte, size)

buffer := bufio.NewReader(publicKeyFile)
_, err = buffer.Read(pembytes)

data, _ := pem.Decode([]byte(pembytes))

publicKeyFile.Close()

publicKeyImported, err := x509.ParsePKIXPublicKey(data.Bytes)

if err != nil {
panic(err)
}

rsaPub, ok := publicKeyImported.(*rsa.PublicKey)

if !ok {
panic(err)
}

return rsaPub
}
func Logout(req *http.Request) error {
authBackend := authentication.InitJWTAuthenticationBackend()
tokenRequest, err := jwt.ParseFromRequest(req, func(token *jwt.Token) (interface{}, error) {
return authBackend.PublicKey, nil
})
if err != nil {
return err
}
tokenString := req.Header.Get("Authorization")
return authBackend.Logout(tokenString, tokenRequest)
}

Can I get all the code?

Yes! of course. Here is a repo with all of the code and tests.

Summary

That’s all you have to do. JWT is a fantastic and simple way to communicate trusted information across untrusted channels. Hope you find a good use for it soon!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store