JWT – Token Based Authentication using GO
Token-based authentication, relies on Token for determine whether the request is authorized or not. And JWT is one of token-based authentication.
Introduction
It’s been a while since my last post in this blog. Well, I trapped in my daily routines, tight deadline and could not get away to find my free time to write an article. Now I have a little freedom and try to write again. Therefore, this one is going to be short one.
In this article I would like to address about token based authentication. This type of authentication emerged in concurrent with the booming of mobile applications. Further, this type of authentication is corroborated by micro services architectures lately. And one of the mechanism to implement token-based authentication is JSON Web-Token (JWT).
If you have no idea what JWT is all about, you can visit this page. The introduction is very concise and informative. Also very helpful for a starter.
Use Case
The use case is very common. I will create an endpoint (or service) for authentication purpose. User then will have to send username and password to authentication service. Whenever the authentication success, the service will return a token, in JWT format, as part of response header.
It seems trivial doesn’t it? To make thing matter, I would like to add a little complexity in this case. After the authentication service generates token for each request, the service will assign a unique ID for each token. Then I will store this unique ID into database with one additional column which contains username and its associated roles. Next, every request has to look up to the ID in the database, to acknowledge whether the ID is valid or not. Moreover, by looking up the associated ID in the database, every request can gather username and roles. This is very useful as part of authorization process (authorization by roles) and logging purpose. Lastly, whenever the user logout from the ecosystem, I will remove his unique ID from the database.
I use this approach with two major considerations:
- By putting only token unique ID in the payload body, I could make the JWT shorter. This could be significant because every request needs to pass the token as part of request header. Furthermore, this approach could resolve some security issue, such as expose the username as part of token payload.
- Even though JWT already has the expiry date, there would be some use case when you want to force the user to logout from the ecosystem. By the lookup ID for every request mechanism, we can force logout by removing the ID from the database. So, if the request do not find the ID, then I will return 401.
Step by Step
For this example, there are several libraries or tools to support the use case:
- SermoDigital for JWT (https://github.com/SermoDigital/jose)
- UUID generator (https://github.com/leonelquinteros/gorand)
- Go-kit
- Key Value Database using Consul
I only show part of the code, which are the security part. I used the similar structure like in my previous post.
Step 1: security.go
First, define the signatures for JWT.
1 2 3 4 |
var ( key = []byte("ru-rocker") method = crypto.SigningMethodHS256 ) |
Next create endpoint, named it JwtEndpoint
. JwtEndpoint function will retrieve consulAddress and consulPort to assign the ID and its associate payload into Consul KV database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
func JwtEndpoint(consulAddress string, consulPort string, log log.Logger) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { req := request.(AuthRequest) response, err = next(ctx, request) if err != nil { return nil, err } resp := response.(AuthResponse) if strings.EqualFold("login", req.Type) { err = loginHandler(consulAddress, consulPort, req.Username, &resp, log) } else if strings.EqualFold("logout", req.Type) { println("logout") err = logoutHandler(consulAddress, consulPort, req, &resp, log) } return resp, err } } } // handling login func loginHandler(consulAddress string, consulPort string, username string, resp *AuthResponse, log log.Logger) error { var ( cid string tokenString string ) defer func(){ log.Log( "username", username, "jwtid", cid, "token", tokenString, ) }() uuid, err := gorand.UUID() if err != nil { panic(err.Error()) } cid = uuid claims := jws.Claims{} m := map[string]interface{} { "username": username, "roles": resp.Roles, } val, _ := json.Marshal(m) claims.SetIssuer("ru-rocker.com") claims.SetIssuedAt(time.Now()) claims.SetExpiration(time.Now().Add(time.Duration(5) * time.Second)) claims.SetJWTID(cid) j := jws.NewJWT(claims, method) b, err := j.Serialize(key) if err != nil { return err } tokenString = string(b[:]) resp.TokenString = tokenString errChan := make(chan error) //register UUID on Consul KV go func() { client := ConsulClient(consulAddress, consulPort, log) kv := client.KV() key := "session/" + uuid p := &api.KVPair{Key: key, Value: []byte(val)} _, e := kv.Put(p, nil) if e != nil { errChan <- e } else { errChan <- nil } }() if err = <- errChan; err != nil { return err } return nil } // handling logout func logoutHandler(consulAddress string, consulPort string, req AuthRequest, resp *AuthResponse, log log.Logger) error { var ( username string cid string tokenString string ) defer func(){ log.Log( "username", username, "jwtid", cid, "token", tokenString, ) }() leeway := 10 * time.Second tokenString = req.TokenString username = req.Username w, err := jws.ParseJWT([]byte(tokenString)) if err != nil { return err } claims := w.Claims() if jwtid, ok := claims.JWTID(); ok { cid = jwtid } err = claims.Validate(time.Now(), leeway, leeway); if err == nil || err == jwt.ErrTokenIsExpired { errChan := make(chan error) //remove UUID on Consul KV go func(){ client := ConsulClient(consulAddress, consulPort, log) kv := client.KV() key := "session/" + cid _, e := kv.Delete (key, nil) resp.TokenString = "" if err != nil { errChan <- err } else if e != nil { errChan <- e } else { errChan <- nil } }() if err = <- errChan; err != nil { return err } else if err == jwt.ErrTokenIsExpired{ return err } } return nil } |
Step 2: transport.go
This one is similar as the previous sample, but only small additional changes to assign the token into response header. The field name for this associated token is X-TOKEN-GEN
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { if e, ok := response.(errorer); ok && e.error() != nil { // Not a Go kit transport error, but a business-logic error. // Provide those as HTTP errors. encodeError(ctx, e.error(), w) return nil } if authResp, ok := response.(AuthResponse); ok { w.Header().Set("X-TOKEN-GEN", authResp.TokenString) } w.Header().Set("Content-Type", "application/json; charset=utf-8") return json.NewEncoder(w).Encode(response) } |
Step 3: main.go
Just a snippet in the main function to include JwtEndpoint as part of request/response.
1 2 3 4 5 6 |
var svc auth.Service svc = auth.AuthService{} svc = auth.LoggingMiddleware(logger)(svc) e := auth.MakeAuthEndpoint(svc) e = auth.JwtEndpoint(*consulAddr, *consulPort, logger)(e) |
Step 4: Execute
Running consul:
1 |
docker run --rm -p 8400:8400 -p 8500:8500 -p 8600:53/udp -h node1 progrium/consul -server -bootstrap -ui-dir /ui |
Running authentication service:
1 2 |
cd $GOPATH/src/github.com/ru-rocker/gokit-playground go run auth/auth.d/main.go -consul.addr localhost -consul.port 8500 -advertise.addr 192.168.1.103 -advertise.port 7002 |
Make a request:
1 |
curl -X POST http://192.168.1.103:7002/auth/login -d'{"username":"admin","password":"password"}' -v |
Response:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
> POST /auth/login HTTP/1.1 > Host: 192.168.1.103:7002 > User-Agent: curl/7.49.0 > Accept: */* > Content-Length: 42 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 42 out of 42 bytes < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 < X-Token-Gen: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDY2NTczOTIsImlhdCI6MTUwNjY1NzM4NywiaXNzIjoicnUtcm9ja2VyLmNvbSIsImp0aSI6ImNmMDI4MTRkLTc3YWQtNGM5ZC1hMGE1LTZhZDFkMmQxYjlmYiJ9.hlROErw7q5l-S3MrxIeM0gWZEM_6F6vwTKsPuoW3x4I < Date: Fri, 29 Sep 2017 03:56:27 GMT < Content-Length: 50 < {"roles":["Admin","User"],"mesg":"Login succeed"} |
Notice X-Token-Gen header in the response header.
Consul KV:
Conclusion
Whenever we want to implement token-based authentication, one thing we can consider is JWT. And implementing JWT via Golang is not a big deal. Thanks to SermoDigital for providing the library. It is quite simple and straight forward.
Also, the KV database from Consul is a great choice as well. Because what we need is only a memory database, with only two columns. So it fits nicely for this purpose.
So far, I talked about authentication process by using token-based process. But authentication will come with another functionality, which is authentication. So I will talk about authorization in my next post. Hopefully :).
You can see the full source code under my repository, under sub-folder auth.
A great article for the OAuth2 Introduction of the Go-kit. I am looking forward for your new Go-kit sample.