What is JWT?
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:
- Header - hashing algorithm and token type
- Payload - data (contains the Claims)
- 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
}
Full Code At: Source
Top comments (0)