Skip to content

Commit

Permalink
Merge pull request #78 from okta/tschaub_cache-config
Browse files Browse the repository at this point in the history
Tschaub cache config
  • Loading branch information
monde authored Feb 17, 2022
2 parents b6068e6 + ae72878 commit f689101
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 59 deletions.
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# Changelog

## v1.1.2
## v1.2.0 (February 16, 2022)

### Updates

- Only `alg` and `kid` claims in a JWT header are considered during verification.
* Customizable resource cache. Thanks, [@tschaub](https://github.com/tschaub)!


## v1.1.3

### Updates

- Fixed edge cause with `aud` claim that would not find Auth0 being JWTs valid (thank you @awrenn).
- Updated readme with testing notes.
- Ran `gofumpt` on code for clean up.
- Ran `gofumpt` on code for clean up.

## v1.1.2

### Updates

- Only `alg` and `kid` claims in a JWT header are considered during verification.

28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This library was built to keep configuration to a minimum. To get it running at

#### Access Token Validation
```go
import github.com/okta/okta-jwt-verifier-golang
import "github.com/okta/okta-jwt-verifier-golang"

toValidate := map[string]string{}
toValidate["aud"] = "api://default"
Expand All @@ -44,7 +44,7 @@ token, err := verifier.VerifyAccessToken("{JWT}")

#### Id Token Validation
```go
import github.com/okta/okta-jwt-verifier-golang
import "github.com/okta/okta-jwt-verifier-golang"

toValidate := map[string]string{}
toValidate["nonce"] = "{NONCE}"
Expand Down Expand Up @@ -80,7 +80,26 @@ verifier := jwtVerifierSetup.New()
verifier.SetLeeway("2m") //String instance of time that will be parsed by `time.ParseDuration`
```

[Okta Developer Forum]: https://devforum.okta.com/
#### Customizable Resource Cache

The verifier setup has a default cache based on
[`patrickmn/go-cache`](https://github.com/patrickmn/go-cache) with a 5 minute
expiry and 10 minute purge setting that is used to store resources fetched over
HTTP. It also defines a `Cacher` interface with a `Get` method allowing
customization of that caching. If you want to establish your own caching
strategy then provide your own `Cacher` object that implements that interface.
Your custom cache is set in the verifier via the `Cache` attribute. See the
example in the [cache example test](utils/cache_example_test.go) that shows a
"forever" cache (that one would never use in production ...)

```go
jwtVerifierSetup := jwtverifier.JwtVerifier{
Cache: NewForeverCache,
// other fields here
}

verifier := jwtVerifierSetup.New()
```

## Testing

Expand All @@ -100,4 +119,5 @@ with Implicit (hybrid) enabled.

```
go test -test.v
```
```

62 changes: 32 additions & 30 deletions adaptors/lestrratGoJwx/lestrratGoJwx.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,61 +19,63 @@ package lestrratGoJwx
import (
"context"
"encoding/json"
"sync"
"time"
"fmt"

"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jws"
"github.com/okta/okta-jwt-verifier-golang/adaptors"
"github.com/patrickmn/go-cache"
"github.com/okta/okta-jwt-verifier-golang/utils"
)

var (
jwkSetCache *cache.Cache = cache.New(5*time.Minute, 10*time.Minute)
jwkSetMu = &sync.Mutex{}
)

func getJwkSet(jwkUri string) (jwk.Set, error) {
jwkSetMu.Lock()
defer jwkSetMu.Unlock()

if x, found := jwkSetCache.Get(jwkUri); found {
return x.(jwk.Set), nil
}
jwkSet, err := jwk.Fetch(context.Background(), jwkUri)
if err != nil {
return nil, err
}

jwkSetCache.SetDefault(jwkUri, jwkSet)

return jwkSet, nil
func fetchJwkSet(jwkUri string) (interface{}, error) {
return jwk.Fetch(context.Background(), jwkUri)
}

type LestrratGoJwx struct {
JWKSet jwk.Set
JWKSet jwk.Set
Cache func(func(string) (interface{}, error)) (utils.Cacher, error)
jwkSetCache utils.Cacher
}

func (lgj LestrratGoJwx) New() adaptors.Adaptor {
func (lgj *LestrratGoJwx) New() adaptors.Adaptor {
if lgj.Cache == nil {
lgj.Cache = utils.NewDefaultCache
}

return lgj
}

func (lgj LestrratGoJwx) GetKey(jwkUri string) {
func (lgj *LestrratGoJwx) GetKey(jwkUri string) {
}

func (lgj LestrratGoJwx) Decode(jwt string, jwkUri string) (interface{}, error) {
jwkSet, err := getJwkSet(jwkUri)
func (lgj *LestrratGoJwx) Decode(jwt string, jwkUri string) (interface{}, error) {
if lgj.jwkSetCache == nil {
jwkSetCache, err := lgj.Cache(fetchJwkSet)
if err != nil {
return nil, err
}
lgj.jwkSetCache = jwkSetCache
}

value, err := lgj.jwkSetCache.Get(jwkUri)
if err != nil {
return nil, err
}

jwkSet, ok := value.(jwk.Set)
if !ok {
return nil, fmt.Errorf("could not cast %v to jwk.Set", value)
}

token, err := jws.VerifySet([]byte(jwt), jwkSet)
if err != nil {
return nil, err
}

var claims interface{}

json.Unmarshal(token, &claims)
if err := json.Unmarshal(token, &claims); err != nil {
return nil, fmt.Errorf("could not unmarshal claims: %s", err.Error())
}

return claims, nil
}
60 changes: 39 additions & 21 deletions jwtverifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,18 @@ import (
"net/http"
"regexp"
"strings"
"sync"
"time"

"github.com/okta/okta-jwt-verifier-golang/adaptors"
"github.com/okta/okta-jwt-verifier-golang/adaptors/lestrratGoJwx"
"github.com/okta/okta-jwt-verifier-golang/discovery"
"github.com/okta/okta-jwt-verifier-golang/discovery/oidc"
"github.com/okta/okta-jwt-verifier-golang/errors"
"github.com/patrickmn/go-cache"
"github.com/okta/okta-jwt-verifier-golang/utils"
)

var (
metaDataCache *cache.Cache = cache.New(5*time.Minute, 10*time.Minute)
metaDataMu = &sync.Mutex{}
regx = regexp.MustCompile(`[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.?([a-zA-Z0-9-_]+)[/a-zA-Z0-9-_]+?$`)
regx = regexp.MustCompile(`[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.?([a-zA-Z0-9-_]+)[/a-zA-Z0-9-_]+?$`)
)

type JwtVerifier struct {
Expand All @@ -49,23 +46,46 @@ type JwtVerifier struct {

Adaptor adaptors.Adaptor

// Cache allows customization of the cache used to store resources
Cache func(func(string) (interface{}, error)) (utils.Cacher, error)

metadataCache utils.Cacher

leeway int64
}

type Jwt struct {
Claims map[string]interface{}
}

func fetchMetaData(url string) (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("request for metadata was not successful: %s", err.Error())
}
defer resp.Body.Close()

metadata := make(map[string]interface{})
if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
return nil, err
}
return metadata, nil
}

func (j *JwtVerifier) New() *JwtVerifier {
// Default to OIDC discovery if none is defined
if j.Discovery == nil {
disc := oidc.Oidc{}
j.Discovery = disc.New()
}

if j.Cache == nil {
j.Cache = utils.NewDefaultCache
}

// Default to LestrratGoJwx Adaptor if none is defined
if j.Adaptor == nil {
adaptor := lestrratGoJwx.LestrratGoJwx{}
adaptor := &lestrratGoJwx.LestrratGoJwx{Cache: j.Cache}
j.Adaptor = adaptor.New()
}

Expand Down Expand Up @@ -291,26 +311,24 @@ func (j *JwtVerifier) validateIss(issuer interface{}) error {
func (j *JwtVerifier) getMetaData() (map[string]interface{}, error) {
metaDataUrl := j.Issuer + j.Discovery.GetWellKnownUrl()

metaDataMu.Lock()
defer metaDataMu.Unlock()

if x, found := metaDataCache.Get(metaDataUrl); found {
return x.(map[string]interface{}), nil
if j.metadataCache == nil {
metadataCache, err := j.Cache(fetchMetaData)
if err != nil {
return nil, err
}
j.metadataCache = metadataCache
}

resp, err := http.Get(metaDataUrl)
value, err := j.metadataCache.Get(metaDataUrl)
if err != nil {
return nil, fmt.Errorf("request for metadata was not successful: %s", err.Error())
return nil, err
}

defer resp.Body.Close()

md := make(map[string]interface{})
json.NewDecoder(resp.Body).Decode(&md)

metaDataCache.SetDefault(metaDataUrl, md)

return md, nil
metadata, ok := value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unable to cast %v to metadata", value)
}
return metadata, nil
}

func (j *JwtVerifier) isValidJwt(jwt string) (bool, error) {
Expand Down
2 changes: 1 addition & 1 deletion jwtverifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func Test_the_verifier_defaults_to_lestrratGoJwx_if_nothing_is_provided_for_adap

jv := jvs.New()

if reflect.TypeOf(jv.GetAdaptor()) != reflect.TypeOf(lestrratGoJwx.LestrratGoJwx{}) {
if reflect.TypeOf(jv.GetAdaptor()) != reflect.TypeOf(&lestrratGoJwx.LestrratGoJwx{}) {
t.Errorf("adaptor did not set to lestrratGoJwx by default. Was set to: %s",
reflect.TypeOf(jv.GetAdaptor()))
}
Expand Down
50 changes: 50 additions & 0 deletions utils/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package utils

import (
"sync"
"time"

"github.com/patrickmn/go-cache"
)

// Cacher is a read-only cache interface.
//
// Get returns the value associated with the given key.
type Cacher interface {
Get(string) (interface{}, error)
}

type defaultCache struct {
cache *cache.Cache
lookup func(string) (interface{}, error)
mutex *sync.Mutex
}

func (c *defaultCache) Get(key string) (interface{}, error) {
c.mutex.Lock()
defer c.mutex.Unlock()

if value, found := c.cache.Get(key); found {
return value, nil
}

value, err := c.lookup(key)
if err != nil {
return nil, err
}

c.cache.SetDefault(key, value)
return value, nil
}

// defaultCache implements the Cacher interface
var _ Cacher = (*defaultCache)(nil)

// NewDefaultCache returns cache with a 5 minute expiration.
func NewDefaultCache(lookup func(string) (interface{}, error)) (Cacher, error) {
return &defaultCache{
cache: cache.New(5*time.Minute, 10*time.Minute),
lookup: lookup,
mutex: &sync.Mutex{},
}, nil
}
50 changes: 50 additions & 0 deletions utils/cache_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package utils_test

import (
"fmt"

jwtverifier "github.com/okta/okta-jwt-verifier-golang"
"github.com/okta/okta-jwt-verifier-golang/utils"
)

// ForeverCache caches values forever
type ForeverCache struct {
values map[string]interface{}
lookup func(string) (interface{}, error)
}

// Get returns the value for the given key
func (c *ForeverCache) Get(key string) (interface{}, error) {
value, ok := c.values[key]
if ok {
return value, nil
}
value, err := c.lookup(key)
if err != nil {
return nil, err
}
c.values[key] = value
return value, nil
}

// ForeverCache implements the read-only Cacher interface
var _ utils.Cacher = (*ForeverCache)(nil)

// NewForeverCache takes a lookup function and returns a cache
func NewForeverCache(lookup func(string) (interface{}, error)) (utils.Cacher, error) {
return &ForeverCache{
values: map[string]interface{}{},
lookup: lookup,
}, nil
}

// Example demonstrating how the JwtVerifier can be configured with a custom Cache function.
func Example() {
jwtVerifierSetup := jwtverifier.JwtVerifier{
Cache: NewForeverCache,
// other fields here
}

verifier := jwtVerifierSetup.New()
fmt.Println(verifier)
}
Loading

0 comments on commit f689101

Please sign in to comment.