Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Clock interface as a time source #263

Merged
merged 1 commit into from
Feb 26, 2024
Merged

feat: Clock interface as a time source #263

merged 1 commit into from
Feb 26, 2024

Conversation

gkats
Copy link
Member

@gkats gkats commented Feb 23, 2024

Verifying a session JWT includes time sensitive validations, like checking token expiration, or that the token can be used now ('nbf' claim).

The HTTP middleware caches JSON web keys for one hour. After that, the cache is considered invalid and a new request to fetch the JSON web key set will be issued.

These are both flows that depend on the current time and time comparisons.

Various system architectures with a distributed nature might not be synchronized. A clock skew is expected.
In such cases it's common to share a "time source" so that all nodes can coordinate on what the perceived current time is.

This commit adds a new clerk.Clock interface which can be passed as a dependency in the http middleware functions and the jwt.Verify function. We also add a dependency to the clockwork project, in order to use a fake clock for our tests.

@gkats gkats requested a review from a team as a code owner February 23, 2024 13:14
@@ -448,6 +448,32 @@ type ClientConfig struct {
BackendConfig
}

// Clock is an interface that can be used with the library instead
// of the [time] package.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To the reviewer: The Clock interface is not a drop-in replacement for the time package.

I've only defined one method on Clock, Now(), because this is the only function of the time package that our library currently needs.

I'm not certain whether we should keep the interface small, or require that it implements all of the time package's available functions, even if we don't currently need them.

The alternative would be to have this interface for clerk.Clock:

type Clock interface {
	After(d time.Duration) <-chan time.Time
	Sleep(d time.Duration)
	Now() time.Time
	Since(t time.Time) time.Duration
	NewTicker(d time.Duration) Ticker
	NewTimer(d time.Duration) Timer
	AfterFunc(d time.Duration, f func()) Timer
}

Let me know what you think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the Now() method is enough for what we are trying to achieve here, expose a way to DI the clock dependency for testing purposes and not only

@@ -9,6 +9,7 @@ require (

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this external dependency, but it's used only in tests. Would be nice to implement our own fake clock. At least there are no other indirect dependencies, the clockwork library is small.

@@ -280,6 +295,15 @@ type cacheEntry struct {
expiresAt time.Time
}

// IsValid returns true if a non-expired entry exists in the cache
// for the provided key, false otherwise.
func (c *jwkCache) IsValid(key string, t time.Time) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Minor but perhaps currentTime or now makes a bit of sense as an argument name?

Suggested change
func (c *jwkCache) IsValid(key string, t time.Time) bool {
func (c *jwkCache) IsValid(key string, now time.Time) bool {

if kid == "" {
return nil, fmt.Errorf("missing jwt kid header claim")
}

jwk := getCache().Get(kid)
if jwk == nil {
if jwk == nil || !getCache().IsValid(kid, clock.Now().UTC()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Not sure if that's difficult but perhaps the clock should be a field of jwkCache ? That would solve the issue of checking for expiration inside Get if we want to retain this approach.

Actually, maybe ignore that I see that getCache is a singleton.

var err error
jwk, err = forceGetJWK(ctx, jwksClient, kid)
if err != nil {
fmt.Println(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧

Suggested change
fmt.Println(err)

if kid == "" {
return nil, fmt.Errorf("missing jwt kid header claim")
}

jwk := getCache().Get(kid)
if jwk == nil {
if jwk == nil || !getCache().IsValid(kid, clock.Now().UTC()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 You can maybe return a cacheEntry object here, something like:

Suggested change
if jwk == nil || !getCache().IsValid(kid, clock.Now().UTC()) {
var cachedJwk cacheEntry
if cachedJwk == nil || !cachedJwk.IsValid(clock.Now().UTC()) {
}
jwk = cachedJwk.Value()

Verifying a session JWT includes time sensitive validations, like
checking token expiration, or that the token can be used now ('nbf'
claim).
The HTTP middleware caches JSON web keys for one hour. After that, the
cache is considered invalid and a new request to fetch the JSON web key
set will be issued.
These are both flows that depend on the current time and time
comparisons.
Various system architectures with a distributed nature might not be
synchronized. A clock skew is expected.
In such cases it's common to share a "time source" so that all nodes can
coordinate on what the perceived current time is.
This commit adds a new clerk.Clock interface which can be passed as a
dependency in the http middleware functions and the jwt.Verify function.
We also add a dependency to the clockwork project, in order to use a
fake clock for our tests.
@gkats gkats merged commit 8f4c6b0 into v2 Feb 26, 2024
4 checks passed
@gkats gkats deleted the core-1719-clock branch February 26, 2024 08:50
@@ -448,6 +448,32 @@ type ClientConfig struct {
BackendConfig
}

// Clock is an interface that can be used with the library instead
// of the [time] package.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the Now() method is enough for what we are trying to achieve here, expose a way to DI the clock dependency for testing purposes and not only


// Now returns the current time.
func (c *defaultClock) Now() time.Time {
return time.Now()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 I believe we should use UTC by default

Suggested change
return time.Now()
return time.Now().UTC()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll update this in a follow up PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants