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

Add callback to NatsAuthOpts that allows refreshing a Token #712

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

Conversation

garrett-sutton
Copy link

@garrett-sutton garrett-sutton commented Jan 13, 2025

Adds the following to NatsAuthOpts

  • Func<Uri, CancellationToken, ValueTask<NatsAuthCred>>? AuthCredCallback

The purpose of this PR is to allow for use cases to refresh a Token, JWT, or NKey associated with their NatsConnection during a reconnect scenario.

resolves #356

@garrett-sutton garrett-sutton marked this pull request as ready for review January 13, 2025 23:18
@garrett-sutton
Copy link
Author

@mtmk are you the right person to review this? Or is there someone else that might be good to reach out to? Thanks!

@mtmk
Copy link
Collaborator

mtmk commented Jan 14, 2025

@garrett-sutton could you sign your commits please?

@garrett-sutton garrett-sutton force-pushed the auth-callback branch 4 times, most recently from 0bdb854 to fb423fb Compare January 14, 2025 23:06
@garrett-sutton
Copy link
Author

@garrett-sutton could you sign your commits please?

Done. As a note, this should probably be called out in CONTRIBUTING.md. I missed this initially because I was only looking there.

@caleblloyd
Copy link
Collaborator

The most recent options callback I reviewed did supply a URI and a CancellationToken as an argument

/// <summary>
/// An optional async callback handler for manipulation of ClientWebSocketOptions used for WebSocket connections.
/// Implementors should use the passed CancellationToken for async operations called by this handler.
/// </summary>
public Func<Uri, ClientWebSocketOptions, CancellationToken, ValueTask>? ConfigureClientWebSocketOptions { get; init; } = null;

API usability point: should there be a single callback rather than individual ones?

Sounds like a good idea, a signature could be

In NatsAuthOpts:

   Func<Uri, CancellationToken, ValueTask<NatsAuthOpts>? Callback { get; init; } = null;

This would have to come with the caveat that a Callback could not return another NatsOptsAuth with a Callback

Or in NatsOpts:

   Func<Uri, CancellationToken, ValueTask<NatsAuthOpts>? AuthOptsCallback { get; init; } = null;

Could also be a slipper slope though, what other options could be updated between reconnects? And which ones could be updated after knowing the URI that will be connected to? Is it worth putting just the auth ones in now, or coming up with a broader approach to allow for updating all potential options

@mtmk
Copy link
Collaborator

mtmk commented Jan 17, 2025

Yes we have started to pass in CT in our callbacks only recently so we should carry on with that. I agree passing back the whole NatsAuthOpts is tricky. I was thinking of a simpler enum with a new type:

enum NatsAuthType { Token, Jwt, NKey, Seed }

struct NatsAuthKey(NatsAuthType type, string Value)

Func<Uri, CancellationToken, ValueTask<NatsAuthKey>? AuthKeyCallback { get; init; } = null;

I feel name AuthKey may not be the name we end up with but this structure would be my proposal.

@garrett-sutton
Copy link
Author

Yes we have started to pass in CT in our callbacks only recently so we should carry on with that. I agree passing back the whole NatsAuthOpts is tricky. I was thinking of a simpler enum with a new type:

enum NatsAuthType { Token, Jwt, NKey, Seed }

struct NatsAuthKey(NatsAuthType type, string Value)

Func<Uri, CancellationToken, ValueTask<NatsAuthKey>? AuthKeyCallback { get; init; } = null;

I feel name AuthKey may not be the name we end up with but this structure would be my proposal.

I like this approach. One follow-up question though. For using something like this, do we actually need to return an array of NatsAuthKey from the callback?

I expect that if the JWT or the NKey change that the Seed should also change. Is that correct? If so, I think we need to provide a way for implementers to specify that multiple things need to be updated.

@mtmk
Copy link
Collaborator

mtmk commented Jan 17, 2025

Yes we have started to pass in CT in our callbacks only recently so we should carry on with that. I agree passing back the whole NatsAuthOpts is tricky. I was thinking of a simpler enum with a new type:

enum NatsAuthType { Token, Jwt, NKey, Seed }

struct NatsAuthKey(NatsAuthType type, string Value)

Func<Uri, CancellationToken, ValueTask<NatsAuthKey>? AuthKeyCallback { get; init; } = null;

I feel name AuthKey may not be the name we end up with but this structure would be my proposal.

I like this approach. One follow-up question though. For using something like this, do we actually need to return an array of NatsAuthKey from the callback?

I expect that if the JWT or the NKey change that the Seed should also change. Is that correct? If so, I think we need to provide a way for implementers to specify that multiple things need to be updated.

you're right. we'd need to pass seed/secret next to value. how about this?

enum NatsAuthType { Token, Jwt, NKey, UserInfo }
 
struct NatsAuthKey(NatsAuthType type, string Value, string Secret)

@caleblloyd
Copy link
Collaborator

you're right. we'd need to pass seed/secret next to value. how about this?

I think the combination of things they may want to supply is:

  • Username + Password at same time
  • Token
  • Seed (from which we can derive the NKey, so no need to really supply the NKey)
  • Jwt + Seed at same time
  • NkeyFile
  • CredsFile
  • No Auth

Too bad TypeUnions aren't in C# yet, it'd be nice to have records for

  • NatsAuthenticator.UsernamePassword(string Username, string Password)
  • NatsAuthenticator.Token(string Token)
  • NatsAuthenticator.NKey(string Seed)
  • NatsAuthenticator.Jwt(string Jwt, string Seed)
  • NatsAuthenticator.NkeyFile(string NKeyFile)
  • NatsAuthenticator.CredsFile(string CredsFile)

Comment on lines 66 to 86
case NatsAuthType.Nkey:
opts.NKey = a.Value;
seed = a.Seed;
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

NKeys and Seeds are a pair. It was probably a mistake in the initial implementation to require passing both, as the NKey can be derived from the seed. I guess it acts as a discriminator so we can tell it apart from JWT + Seed

Copy link
Author

Choose a reason for hiding this comment

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

In the latest, I used the "add a seed" and we can derive the nkey from it approach: 21c5e74

Comment on lines 63 to 78
opts.JWT = a.Value;
seed = a.Seed;
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

JWTs and Seeds are a pair, the Sub in the JWT is the public NKey for the seed

Copy link
Author

Choose a reason for hiding this comment

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

In the static factory methods, I implemented it such that you need to input the jwt and seed for jwt auth but only the seed for nkey auth: 21c5e74

@mtmk
Copy link
Collaborator

mtmk commented Jan 18, 2025

you're right. we'd need to pass seed/secret next to value. how about this?

I think the combination of things they may want to supply is:

  • Username + Password at same time
  • Token
  • Seed (from which we can derive the NKey, so no need to really supply the NKey)
  • Jwt + Seed at same time
  • NkeyFile
  • CredsFile
  • No Auth

Too bad TypeUnions aren't in C# yet, it'd be nice to have records for

  • NatsAuthenticator.UsernamePassword(string Username, string Password)
  • NatsAuthenticator.Token(string Token)
  • NatsAuthenticator.NKey(string Seed)
  • NatsAuthenticator.Jwt(string Jwt, string Seed)
  • NatsAuthenticator.NkeyFile(string NKeyFile)
  • NatsAuthenticator.CredsFile(string CredsFile)

that's good analysis! I'm happy with the name NatsAuthenticator (see edit). we can achieve the same syntax using the above enum + struct combo with these static helpers. One question about JWTs: does the seed change as well or just JWT in which case we could have an overload for that:

enum NatsAuthType { None, Token, Jwt, UserInfo, CredFile .... }

struct NatsAuthenticator
{
    NatsAuthType _type;
    string? _value;
    string? _secret;

    private NatsAuthenticator(NatsAuthType, string?, string?) { ... }
    
    public static NatsAuthenticator NoAuth() { ... }

    public static NatsAuthenticator UsernamePassword(string Username, string Password) { ... }

    public static NatsAuthenticator Token(string Token) { ... }

    public static NatsAuthenticator Jwt(string Jwt, string Seed) { ... }

    public static NatsAuthenticator Jwt(string Jwt) { ... } // maybe?

    ...
}

EDIT: actually just scanned through the changes quick. I feel NatsAuthCred and AuthCredCallback are more inline with our minimalist naming scheme.

string? seed = null;
if (AuthCredCallback != null)
{
using var cts = new CancellationTokenSource(timeout);
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should take in CT from above as param and use it here. if we need the timeout we should link the tokens. The idea is if we're e.g. shutting down callbacks should get the signal as well and quit whatever they might be doing and not hanging.

Copy link
Author

Choose a reason for hiding this comment

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

A question about this. I don't necessarily object to this comment, but I'm not sure of whether 1) we need the timeout and if we do 2) what token to link it to. 3) Otherwise, I'm not sure if it is just OK to pass in CancellationToken.None to this method?

Do you have thoughts on these points?

I took the current approach because it seems to be what other AuthenticateAsync type methods did (i.e. sslConnection.AuthenticateAsClientAsync

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're right we haven't been consistent across the board:

// None
public Func<SslClientAuthenticationOptions, ValueTask>? ConfigureClientAuthentication { get; init; }

// Uses CTS with Connection timeout - but not passing in dispose token from connection object
public Func<Uri, ClientWebSocketOptions, CancellationToken, ValueTask>? ConfigureClientWebSocketOptions { get; init; }

I think we should start using the CT from connection dispose to have a clean shutdown. Should we pass _disposedCancellationTokenSource into Authenticate here:

await _userCredentials.AuthenticateAsync(_clientOpts, WritableServerInfo, _currentConnectUri, Opts.ConnectTimeout, _disposedCancellationTokenSource.Token).ConfigureAwait(false);

then we can link them before passing to the callback here e.g.:

using var cts = new CancellationTokenSource(timeout);

#if NETSTANDARD

using var ctr = cancellationToken.Register(static state => ((CancellationTokenSource)state!).Cancel(), cts);

#else

await using var ctr = cancellationToken.UnsafeRegister(static state => ((CancellationTokenSource)state!).Cancel(), cts);

#endif

var authCred = await AuthCredCallback(uri.Uri, cts.Token).ConfigureAwait(false);

Copy link
Author

Choose a reason for hiding this comment

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

Ok, that makes sense! Thank you for the help on this. Those changes are reflecting in the most recent update of the commit with functional changes: 9088040

Copy link
Collaborator

@caleblloyd caleblloyd left a comment

Choose a reason for hiding this comment

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

I like how the API turned out!

Comment on lines +59 to +63
public string? Username { get; set; } = null;

/// <summary>Connection password (if auth_required is set)</summary>
[JsonPropertyName("pass")]
public string? Password { get; init; } = null;
public string? Password { get; set; } = null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can these two be switched back to init?

Copy link
Author

Choose a reason for hiding this comment

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

I believe they need to have setters because without them the callback can't allow for a client to specify a new username and password (i.e. we are passing in an existing instance of ClientOpts to the AuthenticateAsync method that invokes the new callback).

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes as @garrett-sutton said they are being set here

We should probably make ClientOpts a readonly record all together and return a new ClientOpts object from the auth method. that may be too much change for this PR we can follow up.

@garrett-sutton
Copy link
Author

@mtmk at this point, do you have a feeling on how close we are to approval/merging?

And as a follow-up, how long can I expect until a new release is cut with these changes?

@mtmk
Copy link
Collaborator

mtmk commented Jan 23, 2025

@garrett-sutton I'd like to allow a few days for others to have a chance to review and comment. All being well, I'd say we can merge and release early next week.

@garrett-sutton
Copy link
Author

@mtmk sounds good. I'm looking to utilize the new feature in a new project at my org that we're trying to release soon. I just wanted to check in on where we are at. Thanks!

Copy link
Collaborator

@mtmk mtmk left a comment

Choose a reason for hiding this comment

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

sorry @garrett-sutton a few last minute changes from me nothing too big hopefully.


internal string? Secret { get; }

public static NatsAuthCred NoAuth() => new(NatsAuthType.None, string.Empty, string.Empty);
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should remove this. to me it's not well defined what it's for and I don't believe there is a known use case for it now. I suggest we take it out to avoid confusion unless I'm missing something (sorry I may have suggested it, not sure).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the use case was you had auth before, but you don't now. Or you have a pool of servers, some with auth and some without. So when you see the Url that is being connected to, you can specify no auth if you would like.

I believe this could be a public static readonly NatsAuthCred None though if you like that better

Copy link
Author

Choose a reason for hiding this comment

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

agreed. I removed this in the latest iteration of the functional commit: 333c03b

Copy link
Author

Choose a reason for hiding this comment

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

@caleblloyd Do you have a sense for how likely that workflow could be? I'm torn here that yes, this is technically possible but it seems much less likely than the other workflows. We're also not taking away someone's ability to connect w/o auth.

Curious to hear your thoughts.

Also, impeccable timing on your comment and mine.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think switching auth methods is all that common, but I do like how this Struct represents the Union of all available Auth types. I could see us using it in the future to construct a NatsAuthOpts, so it'd be nice to have the NoAuth option. Also since it's a Struct that does represent an accurate default for an empty struct that is declared. (well it did when the NatsAuthCred enum had None as the default 😄

@@ -1,5 +1,47 @@
namespace NATS.Client.Core;

public enum NatsAuthType
Copy link
Collaborator

Choose a reason for hiding this comment

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

could this be made internal?

Copy link
Author

Choose a reason for hiding this comment

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

changed in the latest iteration of the functional commit: 333c03b

@@ -1,5 +1,47 @@
namespace NATS.Client.Core;

public enum NatsAuthType
{
None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should remove this too as per NoAuth comment elsewhere.

Copy link
Author

Choose a reason for hiding this comment

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

removed in latest iteration of functional commit: 333c03b

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if removing this is a good idea, since NatsAuthCred is a struct if an empty struct is declared, it's going to have the default value of this enum. The None value makes sense there.


public static NatsAuthCred NoAuth() => new(NatsAuthType.None, string.Empty, string.Empty);

public static NatsAuthCred UserInfo(string username, string password)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we call these methods FromUserInfo(), FromToken(), ...? I think that would be more inline with other APIs like TimeSpan.

Copy link
Author

Choose a reason for hiding this comment

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

I did this in the latest iteration of the commit with functional changes: 333c03b

gsutton added 2 commits January 23, 2025 22:25
Add factory methods for NatsAuthCred
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.

Allow updating the Connection's token and/or JWT upon disconnect
4 participants