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

Token cache #19

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Token cache #19

wants to merge 2 commits into from

Conversation

searedcircuit
Copy link

Could do with some polish, but this will cut down on the auth spam. Cursory tests show significant throughput/latency gains.

What this PR does:

  • Cache auth token
  • Read max-age Cache-Control header & set expiration if possible, default to 10 minutes, or honor no-cache/no-store directives
  • Refresh within 30 seconds of expiration

Which issue(s) this PR fixes:
Fixes #18

Checklist

  • Changes manually tested
  • [N/A] Automated Tests added/updated
  • [N/A] Documentation added/updated
  • CLA Signed: DataStax CLA

Could do with some polish, but this will cut down on the auth spam. Cursory tests show significant throughput/latency gains.
- Cache auth token
- Read max-age Cache-Control header & set expiration if possible, default to 10 minutes, or honor no-cache/no-store directives
- Refresh within 30 seconds of expiration
@@ -34,6 +37,11 @@ type authRequest struct {
Password string `json:"password"`
}

var cacheEnabled = true
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think these can be variables of an instance of tableBasedTokenProvider?

Copy link
Author

Choose a reason for hiding this comment

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

Sensible. I'm not totally sure I got it right, but I think I'm close. Works, in any case.

@searedcircuit
Copy link
Author

I'm not sure this will work. There seems to be an outside possibility that the server could forget the token is valid. Currently, the token refresh mechanism is nested in an implementation of the GetRequestMetadata interface in the grpc package, so I can't see a way to trigger a manual refresh from the code that sees the error. I thought about sticking the token on the context, but there are calls that don't use context... open to ideas.

@mpenick
Copy link
Contributor

mpenick commented Feb 4, 2022

Good call. I don't remember all the details on how token auth work in Stargate (I think it might be on a sliding window based on usage). I need a bit of time to refresh my mind on how that work to provide good feedback there. It's almost like we need the token to only be regenerated when the service becomes unauthorized, but this might not be needed if it's on a sliding window. Give a couple days to think about it and I'll get back to you. If you have time you could try checking out the token-auth component in Stargate.

}

// cache expiration default of 10min (arbitrary).
ccExpSec := 600
Copy link
Contributor

@mpenick mpenick Aug 25, 2022

Choose a reason for hiding this comment

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

This shouldn't cache it if there's not a valid Cache-Control. I don't think this should default to an arbitrary value.

@mpenick
Copy link
Contributor

mpenick commented Aug 25, 2022

It looks like the auth service will return a cache control header with max age if there's not an error: https://github.com/stargate/stargate/blob/v1.0.63/auth-api/src/main/java/io/stargate/auth/api/resources/AuthResource.java#L193-L196

@mpenick
Copy link
Contributor

mpenick commented Aug 25, 2022

Here are my suggested changes. The cache logic could be simplified and it shouldn't cache if there's not a cache control header. Also, I think that the provider can be user on multiple threads so any shared data should be protected with a mutex or they should use atomics (more challenging to get correct).

diff --git a/stargate/pkg/auth/client.go b/stargate/pkg/auth/client.go
index d486a8f..a66e75e 100644
--- a/stargate/pkg/auth/client.go
+++ b/stargate/pkg/auth/client.go
@@ -10,6 +10,7 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	log "github.com/sirupsen/logrus"
@@ -17,13 +18,13 @@ import (
 )
 
 type tableBasedTokenProvider struct {
-	cacheEnabled             bool
 	token                    string
 	tokenExpiresAt           time.Time
 	client                   *client
 	username                 string
 	password                 string
 	requireTransportSecurity bool
+	mu                       sync.RWMutex // I believe that the token provider can be access by multiple threads, protect shared data.
 }
 
 type client struct {
@@ -47,7 +48,6 @@ var ccMaxAgeRegex, _ = regexp.Compile(`\d+`)
 // with the returned token.
 func NewTableBasedTokenProvider(serviceURL, username, password string) credentials.PerRPCCredentials {
 	return &tableBasedTokenProvider{
-		cacheEnabled:             true,
 		client:                   getClient(serviceURL),
 		username:                 username,
 		password:                 password,
@@ -59,7 +59,6 @@ func NewTableBasedTokenProvider(serviceURL, username, password string) credentia
 // to false for environments where transport security it not in use.
 func NewTableBasedTokenProviderUnsafe(serviceURL, username, password string) credentials.PerRPCCredentials {
 	return &tableBasedTokenProvider{
-		cacheEnabled:             true,
 		client:                   getClient(serviceURL),
 		username:                 username,
 		password:                 password,
@@ -81,12 +80,14 @@ func (t *tableBasedTokenProvider) GetRequestMetadata(ctx context.Context, uri ..
 }
 
 func (t *tableBasedTokenProvider) getToken(ctx context.Context) (string, error) {
-	// If we have a cached token and it won't expire for at least 30 seconds, use it
-	if t.cacheEnabled &&
-		t.token != "" &&
+	// If we have a cached token that doesn't expire for at least 30 seconds, use it
+	t.mu.RLock()
+	if t.token != "" &&
 		t.tokenExpiresAt.Add(-30*time.Second).After(time.Now().UTC()) {
+		t.mu.RUnlock()
 		return t.token, nil
 	}
+	t.mu.RUnlock()
 
 	authReq := authRequest{
 		Username: t.username,
@@ -126,13 +127,7 @@ func (t *tableBasedTokenProvider) getToken(ctx context.Context) (string, error)
 		return "", fmt.Errorf("error unmarshalling response body: %v", err)
 	}
 
-	// If the server asks, skip the cache.
-	if !t.cacheEnabled {
-		return ar.AuthToken, nil
-	}
-
-	// cache expiration default of 10min (arbitrary).
-	ccExpSec := 600
+	ccExpSec := 0 // Don't cache by default
 
 	// Try to read the server's reported cache expiration, or no-cache (unlikely).
 	ccValue := response.Header.Get("Cache-Control")
@@ -142,26 +137,29 @@ func (t *tableBasedTokenProvider) getToken(ctx context.Context) (string, error)
 				strings.Contains(val, "s-maxage") {
 				res := ccMaxAgeRegex.Find([]byte(ccValue))
 				ccSec, ccErr := strconv.Atoi(string(res))
-				if ccErr == nil {
+				if ccErr != nil {
 					ccExpSec = ccSec
 				}
 				break
 			}
 			if strings.Contains(val, "no-cache") ||
 				strings.Contains(val, "no-store") {
-				t.cacheEnabled = false
-				return ar.AuthToken, nil
+				ccExpSec = 0
+				break
 			}
 		}
 	}
 
-	// Cache the token
-	t.token = ar.AuthToken
-
-	// Set expiration
-	t.tokenExpiresAt = time.Now().UTC().Add(time.Second * time.Duration(ccExpSec))
+	t.mu.Lock()
+	if ccExpSec > 0 {
+		t.token = ar.AuthToken
+		t.tokenExpiresAt = time.Now().UTC().Add(time.Second * time.Duration(ccExpSec))
+	} else {
+		t.token = ""
+	}
+	t.mu.Unlock()
 
-	return t.token, nil
+	return ar.AuthToken, nil
 }
 
 func getClient(serviceURL string) *client {

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.

Auth token is not cached
2 participants