redis / rueidis

A fast Golang Redis client that supports Client Side Caching, Auto Pipelining, Generics OM, RedisJSON, RedisBloom, RediSearch, etc.
Apache License 2.0
2.46k stars 158 forks source link

MS Entra ID Authentication question #619

Closed yash-nisar closed 2 months ago

yash-nisar commented 2 months ago

I wanted to ask if we need to implement the token refresh mechanism by ourselves if we use MS Entra for cache authentication (https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication)

client, err := rueidis.NewClient(rueidis.ClientOption{
    InitAddress:       []string{addr},
    TLSConfig:         tlsConfig,
    AuthCredentialsFn: authCredentialsFn(ctx, logger, clientID),
})
func authCredentialsFn(ctx context.Context, logger logging.Logger, clientID string) func(acc rueidis.AuthCredentialsContext) (rueidis.AuthCredentials, error) {
    return func(acc rueidis.AuthCredentialsContext) (rueidis.AuthCredentials, error) {
        logger.Info("auth: connecting to redis", "username", clientID)
        credProvider := azureCredentialProvider(logger)
        accessToken, err := credProvider(ctx, policy.TokenRequestOptions{Scopes: []string{tokenScope}})
        if err != nil {
            logger.Error(err, "failed to get access token for connecting to redis")
            return rueidis.AuthCredentials{}, err
        }

        return rueidis.AuthCredentials{
            Username: clientID,
            Password: accessToken.Token,
        }, nil
    }
}

This is how we want to do it. My questions are:

rueian commented 2 months ago
  1. Yes, you need to implement the refresh mechanism by yourself.
  2. Yes, the callback is invoked only when establishing a new connection.

I have no experience using MS EntraID, but I guess the token refresh mechanism can be implemented like this:

package main

import (
    "context"
    "github.com/Azure/azure-sdk-for-go/sdk/azcore"
    "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
    "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    "github.com/redis/rueidis"
    "sync"
    "time"
)

func getAzureToken() (azcore.AccessToken, error) {
    cred, err := azidentity.NewDefaultAzureCredential(nil)
    if err != nil {
        return azcore.AccessToken{}, err
    }
    token, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{...})
    if err != nil {
        return azcore.AccessToken{}, err
    }
    return token, nil
}

func main() {
    var mu sync.Mutex
    var token azcore.AccessToken
    client, err := rueidis.NewClient(rueidis.ClientOption{
        AuthCredentialsFn: func(_ rueidis.AuthCredentialsContext) (rueidis.AuthCredentials, error) {
            mu.Lock()
            defer mu.Unlock()
            if token.ExpiresOn.Before(time.Now()) {
                tk, err := getAzureToken()
                if err != nil {
                    return rueidis.AuthCredentials{}, err
                }
                token = tk
            }
            return rueidis.AuthCredentials{Username: "...", Password: token.Token}, nil
        },
    })
    if err != nil {
        panic(err)
    }
    go func() {
        for {
            mu.Lock()
            duration := time.Until(token.ExpiresOn.Add(time.Minute * -10))
            mu.Unlock()
            time.Sleep(duration)
            tk, err := getAzureToken()
            if err != nil {
                continue
            }
            mu.Lock()
            token = tk
            mu.Unlock()
            for _, c := range client.Nodes() {
                c.Do(context.Background(), c.B().Auth().Username("...").Password(token.Token).Build())
            }
        }
    }()
}
yash-nisar commented 2 months ago

Thanks @rueian for your response. I wanted to ask the implications of not implementing this renewal mechanism.

  1. Is a new connection setup immediately when a connection is terminated for some reason ? Is there a possibility of downtime ?
  2. If we have a pubsub connection that breaks for some reason, will it be re-established again ? or do these connections have to be established manually ?
rueian commented 2 months ago

Hi @yash-nisar,

  1. No, a new connection is setup when there is a new request. Old pending read-only requests will be retried by default. Other requests will see errors until the new connection is established.
  2. Yes, the client.Receive will keep retrying to subscribe those provided channels.
yash-nisar commented 2 months ago

@rueian So, if we have 500 requests per second being sent to Azure Redis, does each request enforce a new connection ? or am I misunderstanding something here ?

rueian commented 2 months ago

@yash-nisar,

No, there will be at most 2^PipelineMultiplex connections to each redis node.

yash-nisar commented 2 months ago

Awesome @rueian, really appreciate the prompt response. Last set of questions, I promise 😛

  1. Ok, so if that variable is set to 32, then there will be atmost 32 connections ?
  2. Will those 500 requests will each spin a new connection from this connection pool and release it back to the pool ?
  3. If we don't implement this manual renewal mechanism, do you think there will be downtime ?
rueian commented 2 months ago

Awesome @rueian, really appreciate the prompt response. Last set of questions, I promise 😛

  1. Ok, so if that variable is set to 32, then there will be atmost 32 connections ?
  2. Will those 500 requests will each spin a new connection from this connection pool and release it back to the pool ?
  3. If we don't implement this manual renewal mechanism, do you think there will be downtime ?
  1. If you set PipelineMultiplex=32, then there will be 4,294,967,296 connections.
  2. It is not a connection pool but a multiplexer. 500 requests will be distributed across 2^PipelineMultiplex connections and be pipelined in each connection.
  3. Yes, I think there will be noticeable errors pop up. I suggest implementing the manual renewal mechanism.
yash-nisar commented 2 months ago

Thanks @rueian, closing the issue because all questions were answered ! Really appreciate it :)