Introduction to JWT
JWT (JSON Web Token) is a concise, self-contained method for authentication on the web. It consists of three parts: the header, the payload, and the signature. The header contains the token's type and encryption algorithm information, the payload contains the data to be passed (usually including some permission information and user identification), and the signature is used to verify the integrity and validity of the token.
JWT is mainly used in two scenarios:
- Authentication: Once a user logs in, each subsequent request will include a JWT, allowing the user to access permitted routes, services, and resources.
- Information exchange: JWT is a good method for securely transmitting information between parties, as they can be digitally signed, for example using public/private key pairs.
JWT Data Format
JWT is a compact, URL-safe way to represent information to be exchanged between parties. A JWT actually consists of three parts, separated by dots (.``), namely
Header.Payload.Signature`. Next, we will detail the data formats of these three parts.
1. Header
The header typically consists of two parts: the token type—usually JWT
, and the signature or encryption algorithm used, such as HMAC SHA256
or RSA
. The header is represented in JSON and then encoded into a string using Base64Url. For example:
{
"alg": "HS256",
"typ": "JWT"
}
After encoding, you may get a string similar to this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload
The payload consists of a series of claims, which are information about the entity (usually a user) and other data. The payload can include multiple predefined claims (also known as Registered Claims) and custom claims (Private Claims).
Predefined claims may include:
-
iss
(Issuer): Issuer -
exp
(Expiration Time): Expiration time -
sub
(Subject): Subject -
aud
(Audience): Audience -
iat
(Issued At): Issued time -
nbf
(Not Before): Effective time -
jti
(JWT ID): Unique identity of the JWT
An example payload might look like this (in JSON format):
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
This information will also be encoded with Base64Url, and you may get a string similar to this:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
3. Signature
The signature section is used to sign the two encoded strings mentioned above, in order to verify that the message has not been tampered with during transmission. Firstly, you need to specify a key (if using the HMAC SHA256 algorithm), and then use the algorithm specified in the header to sign the header and payload.
For example, if you have the following header and payload:
HeaderEncoded.HeaderPayload
Pseudocode for signing them using a key might be:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)
The resulting signed string might be:
dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY
Complete JWT
Combining these three parts with a dot (.) as a separator forms the complete JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY
The JWT standard is highly flexible and can store any information you need in the payload (but for the purpose of reducing the JWT size and for security reasons, sensitive information should not be stored), and ensures the integrity of this information through the signature.
Installing the Go JWT library
The Go community provides a package github.com/golang-jwt/jwt
for handling JWT. Installing this library is very simple, just run the following command in your project directory:
go get -u github.com/golang-jwt/jwt/v5
Once installed, you can include it in your import
as follows:
import "github.com/golang-jwt/jwt/v5"
Creating a simple token
To create a simple JWT token using Go language and the HS256 algorithm, you need to follow these steps:
First, generate a new Token object, using the HS256 algorithm:
token := jwt.New(jwt.SigningMethodHS256)
Next, use the SignedString
method to generate a string representation of this token, passing the key you will use for signing.
var mySigningKey = []byte("your-256-bit-secret")
strToken, err := token.SignedString(mySigningKey)
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
fmt.Println(strToken)
This will generate a simple token without any claims.
Creating a token with parameters
One of the main functions of JWT is to carry information. This information is encoded in the Token through claims. For example, let's create custom claims:
// Custom Claims structure
type MyClaims struct {
jwt.RegisteredClaims
Username string `json:"username"`
Admin bool `json:"admin"`
}
// Create a token with custom claims
claims := MyClaims{
RegisteredClaims: jwt.RegisteredClaims{},
Username: "my_username",
Admin: true,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Secret key for signing
var mySigningKey = []byte("your-256-bit-secret")
// Generate the token in string format
strToken, err := token.SignedString(mySigningKey)
if err != nil {
log.Fatalf("Error occurred: %v", err)
}
fmt.Println(strToken)
In the above code, we have defined the MyClaims
struct to contain registered claims as well as some custom information. Then, we still use SignedString
to generate the token string.
Parsing and validating the token
Parsing and validating the JWT is very important, and you can do so as follows:
// We will use the same MyClaims structure
tokenString := "your-JWT-string"
// We need to define a function that the jwt package will use to parse the tokenString
keyFunc := func(t *jwt.Token) (interface{}, error) {
// Verify that the expected signing method is used
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
}
// Return the secret key for the jwt token, in the format []byte, consistent with the key used for signing earlier
return mySigningKey, nil
}
// Parsing the token
claims := &MyClaims{}
parsedToken, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
log.Fatalf("Parsing error: %v", err)
}
if !parsedToken.Valid {
log.Fatalf("Invalid Token")
}
// At this stage, parsedToken has been verified, and we can read the claims
fmt.Printf("User: %s, Admin: %v\n", claims.Username, claims.Admin)
In the above code, we provided the token string, MyClaims
instance, and the key function keyFunc
to the jwt.ParseWithClaims
function. This function will validate the signature and parse the token, filling the claims
variable if the token is valid.