Dwi Wahyudi
Senior Software Engineer (Ruby, Golang, Java)
JWT is useful for signing a web request between parties. By using Golang, I will try to encode a payload into a JWT, and try to decode it.
Overview
- Let say there are 2 parties here: server A and server B.
- Server A wants to send web requests to server B,
- How do server B know that such request is really from server A?
- Can server A and server B share a secret key together Like a password? So server A will just send along that secret key inside the http request to server B, can we do it?
- This can be done but so risky:
- It leaks the secret key in the request,
- Plus we have no guarantee that data has not been tampered/changed by other than server A.
JWT is there to solve those problems. It doesn’t leak the secret key in the request, plus it can make the secret key itself as the message signing key, by using a symmetric key as long as server A and server B share that same key for signing and validating, the message is guaranteed to be safe (not tampered/changed).
Asymmetric key on the other hand require us to create a public and a private key. I will try to cover this topic in the future.
JWT official website has nice introduction about JWT: https://jwt.io/introduction/. Basically a JWT is a compact data which is url-safe, it has 3 segments, separated by dots.
base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)
It’s just segments of base64 encoded texts.
- Header, generally specifies the content type and the algorithm for signature.
{
"alg" : "HS256",
"typ" : "JWT"
}
For this post, I’ll be using HS256 algorithm which uses symmetric key.
2. Payload, this is the data that we want to sign and send, receive and validate. Generally a json data. Payloads (also called claims in JWT) may also contain other data, there are several registered claims specified by IETF: https://tools.ietf.org/html/rfc7519#section-4.1. One of the most common claim is "exp"
, the expiration time, in which JWT is no longer valid.
3. Signature, is encrypted base64 encoded of header separated by dot with base64 encoded of payload.
HMAC-SHA256(secret_key, base64urlEncoding(header) + '.' + base64urlEncoding(payload))
Golang JWT Application
Like usual in Golang app, let’s start create a new project:
go mod init github.com/dwahyudi/golang-jwt-sample
Add some libraries:
go get -u github.com/dgrijalva/jwt-go
go get -u github.com/stretchr/testify
dgrijalva/jwt-go
is the library that we’re going to use. stretchr/testify
is a testing framework.
Here is the directory structure, no main
method, because we’re going run the jwt code (jwt_util.go
) from tests (jwt_util_test.go
).
In order to build and sign a payload, we need the algorithm for signature and the secret key. We’re going to use HS256 algorithm, which uses a symmetric key.
Now, let’s go write our first function. Let’s specify the way to get the secret key from environment variable.
func secretKey() []byte {
var secretKeyBytes = []byte(os.Getenv("GO_JWT_SAMPLE_SECRET_KEY"))
return secretKeyBytes
}
Building and Signing JWT
Now we’re going to use dgrijalva/jwt-go
library to build and sign our JWT. Here is our payload:
{
"user_id": 3
}
Here is the function for building and signing JWT:
func JwtBuildAndSignJSON(userId int) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userId,
})
tokenString, _ := token.SignedString(secretKey())
return tokenString
}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjozfQ.F1555Xd-M9hxINTBwQYc1a-NjJLNPq1V8opiuNq63L4
As you can see, there are 3 segments separated by dots. Like I’ve described above, the first segment is header, the second one is the payload, while the third one is the signature. We can decode the first and second one, but the third one is encrypted and can only be validated by the secret key.
Validating JWT
In order to validate the JWT, we need the same exact key. Here is our function for validating the JWT:
func JwtValidate(tokenString string) (int, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return secretKey(), nil
})
var userId int
if token != nil {
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
var userIdClaim = claims["user_id"]
fetchedUserId := userIdClaim.(float64)
userId = int(fetchedUserId)
}
}
return userId, err
}
Lots of things happened there,
- Code will parse JWT string, JWT string must be in correct format,
- It will check for the algorithm, it has to be HMAC, otherwise it will fail,
- It will validate the signature by using provided secret key.
If any of that 3 fail token
will either be nil
or invalid, and err
won’t be nil.
Testing the Building, Signing and Validating
Valid Signature
Now we can test above operation by writing the test, error should be nil.
func TestSimpleSignAndValidate(t *testing.T) {
var userId = 3
var tokenString = util.JwtBuildAndSignJSON(userId)
var validatedUserId, err = util.JwtValidate(tokenString)
assert.Equal(t, 3, validatedUserId)
assert.Nil(t, err)
}
Malformed JWT
What if we supply the malformed JWT string? Let’s test this as well, error must be present. We must reject this JWT.
func TestValidateMalformedJWT(t *testing.T) {
var tokenString = "ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0=."
var validatedUserId, err = util.JwtValidate(tokenString)
assert.Equal(t, "token contains an invalid number of segments", err.Error())
assert.Equal(t, 0, validatedUserId)
}
Wrong Algorithm
What if we supply "none"
algorithm? Error must be present. We must reject this JWT.
func TestValidateWithNoneAlgorithm(t *testing.T) {
// Base64-encoded of "none" algorithm JOSE header.
var tokenString = "ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0=.ewogICJ1c2VySWQiOiAzLAogICJhdWQiOiAic2FtcGxlLWF1ZGllbmNlIiwKICAiZXhwIjogMTU5Mzg2MTI4NiwKICAiaWF0IjogMTU5Mzg2MDM4NiwKICAiaXNzIjogInNhbXBsZS1pc3N1ZXIiLAogICJzdWIiOiAic2FtcGxlLXVzZXJuYW1lIgp9."
var validatedUserId, err = util.JwtValidate(tokenString)
assert.Equal(t, "Unexpected signing method: none", err.Error())
assert.Equal(t, 0, validatedUserId)
}
Invalid Signature
Then finally we can test if we supply JWT with wrong/different secret key:
Here is the code (for demonstration):
func JwtBuildAndSignJSONAnotherSecret(userId int) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userId,
})
tokenString, _ := token.SignedString([]byte("different-password"))
return tokenString
}
func TestValidateWithDifferentSecret(t *testing.T) {
var userId = 3
var tokenString = util.JwtBuildAndSignJSONAnotherSecret(userId)
var validatedUserId, err = util.JwtValidate(tokenString)
assert.Equal(t, 0, validatedUserId)
assert.Equal(t, "signature is invalid", err.Error())
}
This is the code: https://github.com/dwahyudi/golang-jwt-sample