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")
    }
}

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

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

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

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

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

6 Register the middleware to a Party:

6.1 Or register the middleware to a single route:

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

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

7.1 Get the Context User, if and when the claims implements one or more Context.User methods:

7.2 Get the Verified Token information:

The VerifiedToken looks like this:

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:

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:

When you want to extract the token just from the Authorization Header:

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

And the Verifier, which should decrypt the encrypted payload:

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

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.

Load and Parse keys

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

All the available helpers:

Example Code:

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:

Last updated

Was this helpful?