Categorygithub.com/kernle32dll/jwtcache-go
modulepackage
1.7.0
Repository: https://github.com/kernle32dll/jwtcache-go.git
Documentation: pkg.go.dev

# README

test Go Reference Go Report Card codecov

jwtcache-go

jwtcache-go is a small wrapper lib for caching JWTs.

The initial purpose for developing this lib was caching tokens that were issued for service2service communication.

An exemplary use case for this are Keycloak service accounts.

Download:

go get github.com/kernle32dll/jwtcache-go

Detailed documentation can be found on pkg.go.dev.

Basic usage

TL;DR:

package main

import (
	"github.com/kernle32dll/jwtcache-go"

	"context"
	"log"
)

func main() {
	cache := jwt.NewCache(
		jwt.Name("my cache"),
		jwt.TokenFunction(func(ctx context.Context) (string, error) {
			// ... actually acquire the token, and return it here
			return "someToken", nil
		}),
	)

	token, err := cache.EnsureToken(context.Background())
	if err != nil {
		// oh no...
	}

	log.Printf("got token: %s", token)
}

First, you have to instantiate a jwt.Cache. This is done via jwt.NewCache (which takes option style parameters).

The most important option parameter being the jwt.TokenFunction, which provides a token to the cache if required (either no token is cached yet, or the existing token is expired). Look at pkg.go.dev for other parameters.

With the cache instantiated, you can call the EnsureToken function to start transparently using the cache. Internally, the cache will then use the jwt.TokenFunction to fetch a new token, and cache it afterwards for the validity time provided by the token. Subsequent calls to EnsureToken will then return this cached token, till it expires.

token, err := jwt.EnsureToken(context.Background())

Implementation detail: The validity check is done via the exp claim of the JWT. If it is not set, the token is never cached. However, the token is still passed trough (and a warning is logged).

JWT parser customization

Per default, jwtcache-go ignores JWTs it cannot parse, but still returns them from the token function. However, it is possible to change this via the jwt.RejectUnparsable(true) option.

To make the most of this, you can also adjust the underlying JWT parser, with the jwt.ParseOptions(...) function. For example, you can easily enable signature verification like so:

package main

import (
	"github.com/kernle32dll/jwtcache-go"
	"github.com/lestrrat-go/jwx/jwa"
	jwtParser "github.com/lestrrat-go/jwx/jwt"

	"context"
)

func main() {

	cache := jwt.NewCache(
		jwt.Name("signed cache"),
		jwt.TokenFunction(func(ctx context.Context) (string, error) {
			// ... actually acquire the token, and return it here
			return "someToken", nil
		}),
		// !! HMAC is shown for simplicity - use RSA, ECDSA or EdDSA instead !!
		jwt.ParseOptions(jwtParser.WithVerify(jwa.HS256, []byte("supersecretpassphrase"))),
		jwt.RejectUnparsable(true) // Propagate parsing errors, instead of swallowing them
	)

	_, err := cache.EnsureToken(context.Background())
	if err != nil {
		// this will always happen, as "someToken" is not actually a valid HMAC signed JWT!
	}
}

Advanced usage

In addition to the jwt.Cache, this lib has an additional trick up its sleeve in the form of jwt.CacheMap.

A jwt.CacheMap behaves identically to a jwt.Cache, with the difference that - as the name suggest - the cache is actually a map compromised of several caches.

package main

import (
	"github.com/kernle32dll/jwtcache-go"
	"github.com/lestrrat-go/jwx/jwa"

	"context"
	"log"
)

func main() {
	tenantCache := jwt.NewCacheMap(
		jwt.MapName("my cache map"),
		jwt.MapTokenFunction(func(ctx context.Context, tenantUUID string) (string, error) {
			// ... actually acquire the token, and return it here
			return "some-token", nil
		}),
	)

	token, err := tenantCache.EnsureToken(context.Background(), "d1851563-c529-42d9-994b-6b996ec4b605")
	if err != nil {
		// oh no...
	}

	log.Printf("got token for tenant: %s", token)
}

The use-case jwt.CacheMap was implemented for was multi-tenant applications, which need to independently cache JWTs per tenant (a good cache key might be the UUID of a tenant, for example).

Implementation detail: The underlying map is concurrency-safe, and lazily initialized.

Compatibility

jwt-cache-go is automatically tested against Go 1.15.X and 1.16.X.

# Functions

Headroom sets the headroom on how much earlier the cached token should be considered expired.
Logger sets the logger to be used.
MapHeadroom sets the headroom on how much earlier the cached tokens should be considered expired.
MapLogger sets the logger to be used.
MapName sets the name of the cache.
MapParseOptions set the parse options which are used to parse a JWT.
MapRejectUnparsable sets if the cache should reject (and return the accompanying error) token which are not parsable.
MapTokenFunction set the function which is called to retrieve a new JWT when required.
Name sets the name of the cache.
NewCache returns a new JWT cache.
NewCacheMap returns a new mapped JWT cache.
ParseOptions set the parse options which are used to parse a JWT.
RejectUnparsable sets if the cache should reject (and return the accompanying error) token which are not parsable.
TokenFunction set the function which is called to retrieve a new JWT when required.

# Variables

ErrNotImplemented is the default behavior for the cache, if the token function is not supplied.

# Structs

Cache is a simple caching implementation to reuse JWTs till they expire.
CacheMap is a mapped implementation of Cache, which allows storing JWTs by a key (for example a tenant UUID).

# Interfaces

LoggerContract defines the logging methods required by the cache.

# Type aliases

MapOption represents an option for the mapped cache.
Option represents an option for the cache.