JWT Implementation in Go

ยท 537 words ยท 3 minute read

JWT Overview ๐Ÿ”—

JWTs (short for JSON Web Tokens) are simply a way of safely transmitting information between two parties in a JSON object, which can be signed, encrypted, and verified. It’s commonly used for Bearer tokens in different authentication services (e.g. if you are the “bearer” of a valid token, it grants you access to something).

JWT tokens are broken down into three parts:

  1. Header - hashing algorithm and token type
  2. Payload - data (contains the Claims)
  3. Signature - header + payload + key hashed together

The result of each part is separated by a .. The header and the payload are base64 encoded into the signature before hashed together with your 32 byte secret key. By verifying this signature, we can confirm if any content has been altered during a certain transmission between e.g. an user and a service provider.

An example of a decoded token could be:

"header": {
  "alg": "HS512",
  "typ": "JWT"
}
"payload": {
  "sub": "1234567890",
  "message": "my message",
  "iat": 1516239022
}
HMACSHA512(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-32-byte-secret
)

Both the Header and Payload are JSON objects, and the Signature is simply a combination of them hashed and signed with your key. The object above would result in:

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibWVzc2FnZSI6Im15IG1lc3NhZ2UiLCJpYXQiOjE1MTYyMzkwMjJ9.2Lfy_7c4nI8Jrpuq4hGlY0COq7Vukbuqoy65MGmvuHP_a0v7VMC2KXRM1GYffvQZUshWd2im-IY94k1ptN9TZw

You can play around with the parameters and verify different results at their website

Go Implementation ๐Ÿ”—

In order to implement a JWT system in Go, we simply need the jwt-go package and, optionally, the uuid package.

We first need to define our claims struct:

type UserClaims struct {
  jwt.StandardClaims
  SessionID int64 `json:"session_id"`
}

The claims contain personal information about the user, such as email, name, etc., and it’s mainly used for authentication using these specific identifiers.

We can then define our GenerateNewKey and CreateToken functions:

type key struct {
	key     []byte
	created time.Time
}

var currentKID = ""
var keysMockDB = map[string]key{} // replace with real db

// generate key that'll be used when signing the token
func GenerateNewKey() error {
	newKey := make([]byte, 64)
	_, err := io.ReadFull(rand.Reader, newKey)
	if err != nil {
		return fmt.Errorf("Error in GenerateNewKey while generatinig new Key: %w", err)
	}

	uid := uuid.NewV4()

	keysMockDB[uid.String()] = key{
		key:     newKey,
		created: time.Now(),
	}
	currentKID = uid.String()

	return nil
}

func CreateToken(claims *UserClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodES512, claims)

	signedToken, err := token.SignedString(keysMockDB[currentKID].key)
	if err != nil {
		return "", fmt.Errorf("token.SignedString: %w", err)
	}

	return signedToken, nil
}

Also, we can define a ParseToken function, which would parse a signed token to fetch the claims:

func ParseToken(signedToken string) (*UserClaims, error) {
	token, err := jwt.ParseWithClaims(
		signedToken,
		&UserClaims{},
		func(t *jwt.Token) (interface{}, error) {
			// t is the unverified token to check
			// if same signing algorithm is being used
			if t.Method.Alg() != jwt.SigningMethodES512.Alg() {
				return nil, fmt.Errorf("Invalid signing algorithm")
			}

			// kid is an optional header claim which holds a key identifier
			kid, ok := t.Header["kid"].(string)
			if !ok {
				return nil, fmt.Errorf("Invalid key ID")
			}

			key, ok := keysMockDB[kid]
			if !ok {
				return nil, fmt.Errorf("Invalid key ID")
			}

			return key, nil
		},
	)
	if err != nil {
		return nil, fmt.Errorf("Error in ParseToken while parsing token: %w", err)
	}

	if token.Valid {
		return nil, fmt.Errorf("Error in ParseToken, token is not valid")
	}

	return token.Claims.(*UserClaims), nil
}

Source code ๐Ÿ”—

Github Repository