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.
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).
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!
}
}
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.
jwt-cache-go is automatically tested against Go 1.20
, 1.19
and 1.18
.