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!

@@ -31,6 +32,8 @@ public UserCredentials(NatsAuthOpts authOpts)

public string? Token { get; }

public Func<Task<string>>? TokenHandler { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

to be consistent with other callbacks should we make this ValueTask. Can't remember if we also pass cancellation token. Also what about JWT? would that benefit from a callback like this?

Copy link
Author

Choose a reason for hiding this comment

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

I modified my initial commit to be a ValueTask rather than a Task: 21c6654
From what I can tell in other places, it doesn't appear that a cancellation token is passed.

In 58864dc, I added callbacks for JWT, NKey, and seed. Based on my understanding, if the JWT is being refreshed, the seed also needs to be refreshed. And the same goes with NKeys. I added unit tests to show that the callbacks will take precedence over any raw values that are set on initialization of NatsAuthOpts.

@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.

public string? Token { get; }

public string? Sign(string? nonce)
public Func<ValueTask<string>>? TokenHandler { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

API usability point: should there be a single callback rather than individual ones? would there be a case where application might want to change its auth method? should we also pass in the host, is that helpful in any way?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for single callback on this....

Copy link
Author

Choose a reason for hiding this comment

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

I used your latest suggestion in the other thread in beedf42 to consolidate things into a single callback.

I ended up calling the new structure a NatsAuthCred and largely chose that because you mentioned you weren't thrilled about the idea of NatsAuthKey. I'm open to other ideas on it though.

@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 83
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 75
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

Nkey,
}

public struct NatsAuthCred(NatsAuthType type, string value, string seed)
Copy link
Collaborator

@mtmk mtmk Jan 18, 2025

Choose a reason for hiding this comment

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

We should use a private .ctor and hide the fields. We can expose static factory methods as public only to minimize the API surface not exposing internals.

edit: also I'm guessing it can be a readonly struct

Copy link
Author

Choose a reason for hiding this comment

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

This is done in 21c5e74

gsutton added 2 commits January 20, 2025 14:24
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