JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
When should you use JSON Web Tokens?
Here are some scenarios where JSON Web Tokens are useful:
Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single Sign On is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains.
Information Exchange: JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed—for example, using public/private key pairs—you can be sure the senders are who they say they are. Additionally, as the signature is calculated using the header and the payload, you can also verify that the content hasn't been tampered with.
Examples can be found at: https://github.com/kataras/iris/blob/main/_examples/auth/jwt
Example Code:
packagemainimport ("time""github.com/kataras/iris/v12""github.com/kataras/iris/v12/middleware/jwt")var ( secret = []byte("signature_hmac_secret_shared_key"))typefooClaimsstruct { Foo string`json:"foo"`}funcmain() { app := iris.New() signer := jwt.NewSigner(jwt.HS256, secret, 10*time.Minute)// Enable payload encryption with:// signer.WithEncryption(encKey, nil) app.Get("/", generateToken(signer)) verifier := jwt.NewVerifier(jwt.HS256, secret)// Enable server-side token block feature (even before its expiration time): verifier.WithDefaultBlocklist()// Enable payload decryption with:// verifier.WithDecryption(encKey, nil) verifyMiddleware := verifier.Verify(func() interface{} {returnnew(fooClaims) }) protectedAPI := app.Party("/protected")// Register the verify middleware to allow access only to authorized clients. protectedAPI.Use(verifyMiddleware)// ^ or UseRouter(verifyMiddleware) to disallow unauthorized http error handlers too. protectedAPI.Get("/", protected)// Invalidate the token through server-side, even if it's not expired yet. protectedAPI.Get("/logout", logout) app.Listen(":8080")}funcgenerateToken(signer *jwt.Signer) iris.Handler {returnfunc(ctx iris.Context) { claims :=fooClaims{Foo: "bar"} token, err := signer.Sign(claims)if err !=nil { ctx.StopWithStatus(iris.StatusInternalServerError)return } ctx.Write(token) }}funcprotected(ctx iris.Context) {// Get the verified and decoded claims. claims := jwt.Get(ctx).(*fooClaims)// Optionally, get token information if you want to work with them.// Just an example on how you can retrieve// all the standard claims (set by signer's max age, "exp"). standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) timeLeft := standardClaims.Timeleft() ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)}funclogout(ctx iris.Context) { err := ctx.Logout()if err !=nil { ctx.WriteString(err.Error()) } else { ctx.Writef("token invalidated, a new token is required to access the protected API") }}
The middleware contains two structures, the Signer and the Verifier. The Signer is used to generate tokens and the Verifier to verify the incoming request token.
1. Import in your code import "github.com/kataras/iris/v12/middleware/jwt"
2. Initialize the Signer, outside of a Handler. There are plenty of algorithms options to choose. Let's continue by using HMAC (HS256 shared key, we will use the same on verifier later on):
3. Declare a struct of custom claims (optional, you can use a map too):
// UserClaims a custom claims structure.// * Fields should be exported in order// to be able to decoded later on.typeUserClaimsstruct { Username string`json:"username"`}
4. Generate a token with Sign and send to the client:
7.1 Get the Context User, if and when the claims implements one or more Context.User methods:
user := ctx.User()username, _ := user.GetUsername()
7.2 Get the Verified Token information:
verifiedToken := jwt.GetVerifiedToken(ctx)verifiedToken.Token // the original request token.
The VerifiedToken looks like this:
typeVerifiedTokenstruct { Token []byte// The original token. Header []byte// The header (decoded) part. Payload []byte// The payload (decoded) part. Signature []byte// The signature (decoded) part. StandardClaims Claims// Any standard claims extracted from the payload.}
Token Extractors
By default a new Verifier will extract the token from the ?token=$token URL Parameter or the request Header of Authorization: Bearer $token. To change that behavior you can modify its Extractors []TokenExtractor field.
The TokenExtractor type looks like that:
typeTokenExtractorfunc(iris.Context) string
The middleware provides 3 token extractor helpers:
FromHeader - extracts the token from the Authorization: Bearer {token} header (defaults).
FromQuery - extracts the token from the "token" URL Query Parameter (defaults).
FromJSON(jsonKey string) - extracts the token from a request payload(body) with a key of "jsonKey", e.g. FromJSON("access_token") will retrieve the token from a request body of: {"access_token": "$TOKEN", ...}.
The Verifier.Extractors field (the result of the NewVerifier function) defaults to: []TokenExtractor{FromHeader, FromQuery}, so it tries to read the token from the Authorization: Bearer header and if not found then it tries to extract it through the "token" URL Query Parameter. You can always customize this slice field to match your application's requirements, example of adding a custom extractor:
verifier := jwt.NewVerifier(jwt.HS256, []byte("secret"))verifier.Extractors =append(verifier.Extractors, func(ctx iris.Context) string {// Return the raw token string from the request.//// For the sake of the example, we will try to// retrieve the token, if all previous extractors failed,// through a "X-Token" custom header:return ctx.GetHeader("X-Token")})
When you want to extract the token just from the Authorization Header:
JWE (encrypted JWTs) is outside the scope of this middleware, a wire encryption of the token's payload is offered to secure the data instead. If the application requires to transmit a token which holds private data then it needs to encrypt the data on Sign and decrypt on Verify. The Signer.WithEncryption and Verifier.WithDecryption methods can be called to apply any type of encryption.
The middleware offers one of the most popular and common way to secure data; the GCM mode + AES cipher. We follow the encrypt-then-sign flow which most researchers recommend (it's safer as it prevents padding oracle attacks).
signer := jwt.NewSigner(jwt.HS256, []byte("secret"), 15*time.Minute)// The key argument should be the AES key,// either 16, 24, or 32 bytes to select// AES-128, AES-192, or AES-256.//// The second argument is the additional data, can be nil.signer.WithEncryption([]byte("itsa16bytesecret"), nil)
And the Verifier, which should decrypt the encrypted payload:
When a user logs out, the client app should delete the token from its memory. This would stop the client from being able to make authorized requests. But if the token is still valid and somebody else has access to it, the token could still be used. Therefore, a server-side invalidation is indeed useful for cases like that. When the server receives a logout request, take the token from the request and store it to the Blocklist through the Context.Logout method. For each authorized request the Verifier.Verify will check the Blocklist to see if the token has been invalidated. To keep the search space small, the expired tokens are automatically removed from the Blocklist.
Iris JWT Middleware has two versions of a Blocklist: In-Memory and Redis.
Example Code (read the comments carefully):
packagemainimport ("time""github.com/kataras/iris/v12""github.com/kataras/iris/v12/middleware/jwt""github.com/kataras/iris/v12/middleware/jwt/blocklist/redis"// Optionally to set token identifier."github.com/google/uuid")var ( signatureSharedKey = []byte("sercrethatmaycontainch@r32length") signer = jwt.NewSigner(jwt.HS256, signatureSharedKey, 15*time.Minute) verifier = jwt.NewVerifier(jwt.HS256, signatureSharedKey))typeuserClaimsstruct { Username string`json:"username"`}funcmain() { app := iris.New()// IMPORTANT//// To use the in-memory blocklist just:// verifier.WithDefaultBlocklist()// To use a persistence blocklist, e.g. redis,// start your redis-server and: blocklist := redis.NewBlocklist()// To configure single client or a cluster one:// blocklist.ClientOptions.Addr = "127.0.0.1:6379"// blocklist.ClusterOptions.Addrs = []string{...}// To set a prefix for jwt ids:// blocklist.Prefix = "myapp-"//// To manually connect and check its error before continue:// err := blocklist.Connect()// By default the verifier will try to connect, if failed then it will throw http error.//// And then register it: verifier.Blocklist = blocklist verifyMiddleware := verifier.Verify(func() interface{} {returnnew(userClaims) }) app.Get("/", authenticate) protectedAPI := app.Party("/protected", verifyMiddleware) protectedAPI.Get("/", protected) protectedAPI.Get("/logout", logout)// http://localhost:8080// http://localhost:8080/protected?token=$token// http://localhost:8080/logout?token=$token// http://localhost:8080/protected?token=$token (401) app.Listen(":8080")}funcauthenticate(ctx iris.Context) { claims :=userClaims{ Username: "kataras", }// Generate JWT ID. random, err := uuid.NewRandom()if err !=nil { ctx.StopWithError(iris.StatusInternalServerError, err)return } id := random.String()// Set the ID with the jwt.ID. token, err := signer.Sign(claims, jwt.ID(id))if err !=nil { ctx.StopWithError(iris.StatusInternalServerError, err)return } ctx.Write(token)}funcprotected(ctx iris.Context) { claims := jwt.Get(ctx).(*userClaims)// To the standard claims, e.g. the generated ID:// jwt.GetVerifiedToken(ctx).StandardClaims.ID ctx.WriteString(claims.Username)}funclogout(ctx iris.Context) { ctx.Logout() ctx.Redirect("/", iris.StatusTemporaryRedirect)}
By default the unique identifier is retrieved through the "jti" (Claims{ID}) and if that it's empty then the raw token is used as the map key instead. To change that behavior simply modify the blocklist.GetKey field.
JSON Web Algorithms
There are several types of signing algorithms available according to the JWA(JSON Web Algorithms) spec. The specification requires a single algorithm to be supported by all conforming implementations:
HMAC using SHA-256, called HS256 in the JWA spec.
The specification also defines a series of recommended algorithms:
RSASSA PKCS1 v1.5 using SHA-256, called RS256 in the JWA spec.
ECDSA using P-256 and SHA-256, called ES256 in the JWA spec.
The implementation supports all of the above plus RSA-PSS and the new Ed25519. Navigate to the alg.go source file for details. In-short:
Choosing the best algorithm for your application needs is up to you, however, my recommendations follows.
Already work with RSA public and private keys? Choose RSA(RS256/RS384/RS512/PS256/PS384/PS512) (length of produced token characters is bigger).
If you need the separation between public and private key, choose ECDSA(ES256/ES384/ES512) or EdDSA. ECDSA and EdDSA produce smaller tokens than RSA.
If you need performance and well-tested algorithm, choose HMAC(HS256/HS384/HS512) - the most common method.
The basic difference between symmetric and an asymmetric algorithm is that symmetric uses one shared key for both signing and verifying a token, and the asymmetric uses private key for signing and a public key for verifying. In general, asymmetric data is more secure because it uses different keys for the signing and verifying process but it's slower than symmetric ones.
Use your own Algorithm
If you ever need to use your own JSON Web algorithm, just implement the Alg interface. Pass it on jwt.Sign and jwt.Verify functions and you're ready to GO.
Generate keys
Keys can be generated via OpenSSL or through Go's standard library.
Access tokens are used to identify a user without tapping into the database.
Refresh tokens are used for refreshing expired access tokens. After an access token expires, the refresh token is used to get a new pair of access and refresh tokens.
Example Code:
packagemainimport ("fmt""time""github.com/kataras/iris/v12""github.com/kataras/iris/v12/middleware/jwt")const ( accessTokenMaxAge =10* time.Minute refreshTokenMaxAge = time.Hour)var ( privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem") signer = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge) verifier = jwt.NewVerifier(jwt.RS256, publicKey))// UserClaims a custom access claims structure.typeUserClaimsstruct { ID string`json:"user_id"`// Do: `json:"username,required"` to have this field required// or see the Validate method below instead. Username string`json:"username"`}// GetID implements the partial context user's ID interface.// Note that if claims were a map then the claims value converted to UserClaims// and no need to implement any method.//// This is useful when multiple auth methods are used (e.g. basic auth, jwt)// but they all share a couple of methods.func (u *UserClaims) GetID() string {return u.ID}// GetUsername implements the partial context user's Username interface.func (u *UserClaims) GetUsername() string {return u.Username}// Validate completes the middleware's custom ClaimsValidator.// It will not accept a token which its claims missing the username field// (useful to not accept refresh tokens generated by the same algorithm).func (u *UserClaims) Validate() error {if u.Username =="" {return fmt.Errorf("username field is missing") }returnnil}// For refresh token, we will just use the jwt.Claims// structure which contains the standard JWT fields.funcmain() { app := iris.New() app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized) app.Get("/authenticate", generateTokenPair) app.Get("/refresh", refreshToken) protectedAPI := app.Party("/protected") { verifyMiddleware := verifier.Verify(func() interface{} {returnnew(UserClaims) }) protectedAPI.Use(verifyMiddleware) protectedAPI.Get("/", func(ctx iris.Context) {// Access the claims through: jwt.Get:// claims := jwt.Get(ctx).(*UserClaims)// ctx.Writef("Username: %s\n", claims.Username)//// OR through context's user (if at least one method was implement by our UserClaims): user := ctx.User() id, _ := user.GetID() username, _ := user.GetUsername() ctx.Writef("ID: %s\nUsername: %s\n", id, username) }) }// http://localhost:8080/protected (401)// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})// http://localhost:8080/protected?token={access_token} (200)// http://localhost:8080/protected?token={refresh_token} (401)// http://localhost:8080/refresh?refresh_token={refresh_token}// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})// http://localhost:8080/refresh?refresh_token={access_token} (401) app.Listen(":8080")}funcgenerateTokenPair(ctx iris.Context) {// Simulate a user... userID :="53afcf05-38a3-43c3-82af-8bbbe0e4a149"// Map the current user with the refresh token,// so we make sure, on refresh route, that this refresh token owns// to that user before re-generate. refreshClaims :=jwt.Claims{Subject: userID} accessClaims :=UserClaims{ ID: userID, Username: "kataras", }// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.// First argument is the access claims,// second argument is the refresh claims,// third argument is the refresh max age. tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge)if err !=nil { ctx.Application().Logger().Errorf("token pair: %v", err) ctx.StopWithStatus(iris.StatusInternalServerError)return }// Send the generated token pair to the client.// The tokenPair looks like: {"access_token": $token, "refresh_token": $token} ctx.JSON(tokenPair)}// There are various methods of refresh token, depending on the application requirements.// In this example we will accept a refresh token only, we will verify only a refresh token// and we re-generate a whole new pair. An alternative would be to accept a token pair// of both access and refresh tokens, verify the refresh, verify the access with a Leeway time// and check if its going to expire soon, then generate a single access token.funcrefreshToken(ctx iris.Context) {// Assuming you have access to the current user, e.g. sessions.//// Simulate a database call against our jwt subject// to make sure that this refresh token is a pair generated by this user.// * Note: You can remove the ExpectSubject and do this validation later on by yourself. currentUserID :="53afcf05-38a3-43c3-82af-8bbbe0e4a149"// Get the refresh token from ?refresh_token=$token OR// the request body's JSON{"refresh_token": "$token"}. refreshToken := []byte(ctx.URLParam("refresh_token"))iflen(refreshToken) ==0 {// You can read the whole body with ctx.GetBody/ReadBody too.var tokenPair jwt.TokenPairif err := ctx.ReadJSON(&tokenPair); err !=nil { ctx.StopWithError(iris.StatusBadRequest, err)return } refreshToken = tokenPair.RefreshToken }// Verify the refresh token, which its subject MUST match the "currentUserID". _, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})if err !=nil { ctx.Application().Logger().Errorf("verify refresh token: %v", err) ctx.StatusCode(iris.StatusUnauthorized)return }/* Custom validation checks can be performed after Verify calls too: currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" userID := verifiedToken.StandardClaims.Subject if userID != currentUserID { ctx.StopWithStatus(iris.StatusUnauthorized) return } */// All OK, re-generate the new pair and send to client,// we could only generate an access token as well.generateTokenPair(ctx)}funchandleUnauthorized(ctx iris.Context) {if err := ctx.GetErr(); err !=nil { ctx.Application().Logger().Errorf("unauthorized: %v", err) } ctx.WriteString("Unauthorized")}