JSON Web Tokens

Introduction

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.

Read more at: https://jwt.io/introduction

Using JWT with Iris

The Iris JWT middleware was designed with security, performance and simplicity in mind, it protects your tokens from critical vulnerabilities that you may find in other libraries. It is based on the kataras/jwt package.

Examples can be found at: https://github.com/kataras/iris/blob/main/_examples/auth/jwt

Example Code:

package main

import (
    "time"

    "github.com/kataras/iris/v12"
    "github.com/kataras/iris/v12/middleware/jwt"
)

var (
    secret = []byte("signature_hmac_secret_shared_key")
)

type fooClaims struct {
    Foo string `json:"foo"`
}

func main() {
    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{} {
        return new(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")
}

func generateToken(signer *jwt.Signer) iris.Handler {
    return func(ctx iris.Context) {
        claims := fooClaims{Foo: "bar"}

        token, err := signer.Sign(claims)
        if err != nil {
            ctx.StopWithStatus(iris.StatusInternalServerError)
            return
        }

        ctx.Write(token)
    }
}

func protected(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)
}

func logout(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")
    }
}
http://localhost:8080
http://localhost:8080/protected?token=$token (or Authorization: Bearer $token)
http://localhost:8080/protected/logout?token=$token
http://localhost:8080/protected?token=$token (401)

Step by Step

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):

signer := jwt.NewSigner(jwt.HS256, []byte("secret"), 15*time.Minute)

Arguments of NewSigner:

  1. The Signature Algorithm

  2. The Signature's private (or shared) key

  3. Expiration duration

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.
type UserClaims struct {
    Username string `json:"username"`
}

4. Generate a token with Sign and send to the client:

app.Post("/signin", func(ctx iris.Context) {
    claims := UserClaims{
        Username: "kataras",
    }

    token, err := signer.Sign(claims)
    if err != nil {
        ctx.StopWithStatus(iris.StatusInternalServerError)
        return
    }

    ctx.Write(token)
})

4 To verify a token, initialize a Verifier outside of a Handler:

verifier := jwt.NewVerifier(jwt.HS256, []byte("secret"))

5 Create a middleware, outsode of a Handler, for a specific claims type:

verifyMiddleware := verifier.Verify(func() interface{} {
    return new(UserClaims) // must return a pointer to the claims struct type.
})

6 Register the middleware to a Party:

protected := app.Party("/protected")
protected.Use(verifyMiddleware)

6.1 Or register the middleware to a single route:

app.Get("/todos", verifyMiddleware, getTodos)

6.2 Or call the middleware from inside a Handler (not recommended):

handler := func(ctx iris.Context) {
    ok := ctx.Proceed(verifyMiddleware)
    if !ok {
        ctx.StopWithStatus(iris.StatusUnauthorized)
        return
    }

    claims := jwt.Get(ctx).(*UserClaims)
}

app.Get("/protected", handler)

7 Get the claims from a route handler using the package-level jwt.Get function:

protected.Get("/todos", func(ctx iris.Context) {
    claims := jwt.Get(ctx).(*UserClaims)
    ctx.WriteString(claims.Username)
})

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:

type VerifiedToken struct {
	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:

type TokenExtractor func(iris.Context) string

The middleware provides 3 token extractor helpers:

  1. FromHeader - extracts the token from the Authorization: Bearer {token} header (defaults).

  2. FromQuery - extracts the token from the "token" URL Query Parameter (defaults).

  3. 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:

verifier.Extractors = []jwt.TokenExtractor{jwt.FromHeader}

Token Payload Encryption

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:

verifier := jwt.NewVerifier(jwt.HS256, []byte("secret"))
verifier.WithDecryption([]byte("itsa16bytesecret"), nil)
// [...]

Block a Token

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):

package main

import (
	"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)
)

type userClaims struct {
	Username string `json:"username"`
}

func main() {
	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{} {
		return new(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")
}

func authenticate(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)
}

func protected(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)
}

func logout(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:

Choose the right Algorithm

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.

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/elliptic"
    "crypto/ed25519"
)
// Generate HMAC
sharedKey := make([]byte, 32)
_, _ = rand.Read(sharedKey)

// Generate RSA
bitSize := 2048
privateKey, _ := rsa.GenerateKey(rand.Reader, bitSize)
publicKey := &privateKey.PublicKey

// Generace ECDSA
c := elliptic.P256()
privateKey, _ := ecdsa.GenerateKey(c, rand.Reader)
publicKey := &privateKey.PublicKey

// Generate EdDSA
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)

Load and Parse keys

This package contains all the helpers you need to load and parse PEM-formatted keys.

All the available helpers:

// HMAC
MustLoadHMAC(filenameOrRaw string) []byte
// RSA
MustLoadRSA(privFile, pubFile string) (*rsa.PrivateKey, *rsa.PublicKey)
LoadPrivateKeyRSA(filename string) (*rsa.PrivateKey, error)
LoadPublicKeyRSA(filename string) (*rsa.PublicKey, error) 
ParsePrivateKeyRSA(key []byte) (*rsa.PrivateKey, error)
ParsePublicKeyRSA(key []byte) (*rsa.PublicKey, error)
// ECDSA
MustLoadECDSA(privFile, pubFile string) (*ecdsa.PrivateKey, *ecdsa.PublicKey)
LoadPrivateKeyECDSA(filename string) (*ecdsa.PrivateKey, error)
LoadPublicKeyECDSA(filename string) (*ecdsa.PublicKey, error) 
ParsePrivateKeyECDSA(key []byte) (*ecdsa.PrivateKey, error)
ParsePublicKeyECDSA(key []byte) (*ecdsa.PublicKey, error)
// EdDSA
MustLoadEdDSA(privFile, pubFile string) (ed25519.PrivateKey, ed25519.PublicKey)
LoadPrivateKeyEdDSA(filename string) (ed25519.PrivateKey, error)
LoadPublicKeyEdDSA(filename string) (ed25519.PublicKey, error)
ParsePrivateKeyEdDSA(key []byte) (ed25519.PrivateKey, error)
ParsePublicKeyEdDSA(key []byte) (ed25519.PublicKey, error)

Example Code:

import "github.com/kataras/iris/v12/middleware/jwt"
privateKey, publicKey := jwt.MustLoadEdDSA("./private_key.pem", "./public_key.pem")

signer := jwt.NewSigner(jwt.EdDSA, privateKey, 15*time.Minute)
verifier := jwt.NewVerifier(jwt.EdDSA, publicKey)

Refresh Token

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:

package main

import (
	"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.
type UserClaims struct {
	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")
	}

	return nil
}

// For refresh token, we will just use the jwt.Claims
// structure which contains the standard JWT fields.

func main() {
	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{} {
			return new(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")
}

func generateTokenPair(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.
func refreshToken(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"))
	if len(refreshToken) == 0 {
		// You can read the whole body with ctx.GetBody/ReadBody too.
		var tokenPair jwt.TokenPair
		if 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)
}

func handleUnauthorized(ctx iris.Context) {
	if err := ctx.GetErr(); err != nil {
		ctx.Application().Logger().Errorf("unauthorized: %v", err)
	}

	ctx.WriteString("Unauthorized")
}

Last updated