Skip to content

Unable to reassign the Authenticator property in v109 #2024

Closed

Description

@Balkoth

Describe the bug
The changes introduced by making the Authenticator on RestClient immutable makes it impossible to use the same RestClient for authentication and after successfully authentication setting the Authenticator for further method calls which require the client to be authenticated.

To Reproduce

internal sealed class CustomRestClient : IDisposable
{
  private readonly RestClient _restClient;

  public CustomRestClient(string baseUrl) => _restClient = new RestClient(baseUrl);  

  public void Dispose() => _restClient.Dispose();

  public async Task<bool> Authenticate(string clientId, string clientSecret)
  {
    RestRequest request = new RestRequest("api/oauth2/token");
    request.AddParameter("grant_type", "client_credentials");
    request.AddParameter("client_id", clientId);
    request.AddParameter("client_secret", clientSecret);
  
    AppOAuthToken? response = await _restClient.PostAsync<AppOAuthToken>(request);
    _restClient.Authenticator = response != null && response.AccessToken != null ? new JwtAuthenticator(response.AccessToken) : default;
  
    return response != null && response.AccessToken != null;
  }
}

Expected behavior
The docs state that a single instance of RestClient should be used to prevent starvation.

Desktop (please complete the following information):

  • OS: Windows 10 21H2
  • .NET 7
  • Version 109.0.1

Activity

  1. Balkoth commented on Mar 13, 2023

    @Balkoth
    Author

    Working around this by initializing the RestClient with a JwtAuthenticator with an invalid token and using this to authenticate against the rest endpoint. Then after succesfully authenticating use SetBearerToken to set the token on the JwtAuthenticator for further method calls.

    internal sealed class CustomRestClient : IDisposable
    {
      private const string EMPTY_TOKEN = "EMPTY_TOKEN";
    
      private readonly RestClient _restClient;
    
      public CustomRestClient(string baseUrl) => _restClient = new RestClient(new RestClientOptions(baseUrl) { Authenticator = new JwtAuthenticator(EMPTY_TOKEN) });
    
      public void Dispose() => _restClient.Dispose();
    
      public async Task<bool> Authenticate(string clientId, string clientSecret)
      {
        RestRequest request = new RestRequest("api/oauth2/token");
        request.AddParameter("grant_type", "client_credentials");
        request.AddParameter("client_id", clientId);
        request.AddParameter("client_secret", clientSecret);
      
        AppOAuthToken? response = await _restClient.PostAsync<AppOAuthToken>(request);
        ((JwtAuthenticator)_restClient.Options.Authenticator!).SetBearerToken(response != null && response.AccessToken != null ? response.AccessToken : EMPTY_TOKEN);
      
        return response != null && response.AccessToken != null;
      }
    }

    I don't know if any other, than the rest endpoint i currently use, support this behaviour, so use with caution.

  2. alexeyzimarev commented on Mar 13, 2023

    @alexeyzimarev
    Member

    The docs suggests using a single instance for making frequent calls. If you are doing a single call for authentication purposes you can use a separate, one-time-use client instance.

    You can also hold the authenticator instance inside your wrapper:

    JwtAuthenticator _authenticator;
    
    public CustomRestClient(string baseUrl) {
        _authenticator = new JwtAuthenticator(EMPTY_TOKEN)
        _restClient = new RestClient(new RestClientOptions(baseUrl) { 
            Authenticator = authenticator 
        });
    }
    

    and then call SetBearerToken when you have it.

    However, if you look at my old Twitter OAuth2 authenticator sample, it makes a call on-demand using a separate client instance inside the authenticator. It allows you to have full control over the flow.

    I think the main deficiency of the authenticator approach with tokens is that it doesn't support token refresh. I believe we need a separate issue to discuss most common scenarios. Getting the token using grant type, client id and secret is very common. But the OIDC token and authentication token, for example, work differently.

    It would be great to document specific flows in that new issue to figure out a workable solution, as we all know that the IAuthenticator abstraction is too weak.

  3. changed the title [-]Commit #7e0a1af breaks existing scenario of using a single RestClient[/-] [+]Unable to reassign the Authenticator property in v109[/+] on Mar 13, 2023
  4. alexeyzimarev commented on Mar 29, 2023

    @alexeyzimarev
    Member

    One comment about this:

    The docs state that a single instance of RestClient should be used to prevent starvation.

    That is true, but how can a single additional client instance only used for authentication (once) can cause starvation? The infamous issue with too many open sockets is caused by repeated creation of message handlers, which is not the case when we are talking about a single call to get a token.

  5. rassilon commented on Mar 29, 2023

    @rassilon

    @alexeyzimarev , while migrating a C# REST library to 109 I ended up with this authenticator based on your old twitter example.

    It is assisted with this additional logic in the core call to RestSharpClient Execute method:

                response = _restSharpClient.Execute(request);
                // NOTE: Completed was returned with earlier versions of RestSharp
                // (Pre 109.x)
                if ((response.ResponseStatus == ResponseStatus.Completed
                     || response.ResponseStatus == ResponseStatus.Error)
                    && response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    _restSharpClient.Options.Authenticator.Authenticate(_restSharpClient, request);
                    response = _restSharpClient.Execute(request);
                }
    

    The authenticator uses an AsyncLock to ensure only one additional RestClient gets created for the new token aquistion.

    FYI...

  6. alexeyzimarev commented on Mar 30, 2023

    @alexeyzimarev
    Member

    I'd really like to find out how authenticators can be made better. Checking the status code for 401 was one of my thoughts too. If you have a proposal for the authentication flow and related changes in code, please write here.

  7. rassilon commented on Mar 30, 2023

    @rassilon

    Given how complex and fractured the authentication space is I think the only easy addition would be to add some assistance to the supplied Authenticator for 401 handling for expired/revoked tokens.
    Maybe something like this: (pseudo-code)

           <inside Execute method>:
           If response status is 401:
               if request included an auth header value: 
                    add some additional state to pass to .Authenticate call to hint that it might be a re-auth (either due to expiration, or token revocation, the cause really isn't important of course)
                    if the returned token is different:
                        re-execute the same url with the new token from the authenticator
                    else: 
                         nothing else to do, let the caller deal with the problem
    

    Determining if the request has an auth header could be done a couple of different ways if more than just 'Authorization' needs to be supported.

  8. rassilon commented on Mar 30, 2023

    @rassilon

    Additionally, if Authenticators could somehow re-use the existing RestClient instance instead of needing to create another one to perform the authentication request with, that'd be nice as well but that seems like a tricky road to go down.

  9. rassilon commented on Mar 31, 2023

    @rassilon

    After checking out: https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/

    It seems like there should be a second method for authenticators: AuthenticateWithFailedResponse or something so that the Authenticator can choose to examine the content of the failed response for error conditions, decide to utilize a refresh token, or just re-request the token based on whatever OAuth flow that is being used.

    So, the above pseudo-code might look like this instead:

           <inside Execute method>:
           If response status is 401:
               if request included an auth header value: 
                    call Authenticator.AuthenticateWithFailedRespond(RestResponse)
                    if the returned token is different:
                        re-execute the same url with the new token from the authenticator
                        return the new response
                    else: 
                         nothing else to do, let the caller deal with the problem and just return the original response
    
  10. stale commented on May 22, 2023

    @stale

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

  11. repo-ranger commented on May 22, 2023

    @repo-ranger

    ⚠️ This issue has been marked wontfix and will be closed in 3 days

  12. abc123xxx commented on Mar 5, 2024

    @abc123xxx

    Is there anything new on the subject?
    I also need to be able to change the authenticator after creating the RestClient.

  13. rassilon commented on Mar 5, 2024

    @rassilon

    Is there anything new on the subject? I also need to be able to change the authenticator after creating the RestClient.

    Could you explain your use case a little here? I'm not sure why you'd need more than one authenticator after creating the RestClient.

  14. abc123xxx commented on Mar 6, 2024

    @abc123xxx

    I have a specific use case in which I first have to use an OAuth1Authenticator via key and secret when logging in to a web API and then have to do another authentication with an OAuth1Authenticator. The access then takes place via another OAuth1Authenticator.

    So I first need an Access Token and then a RequestToken etc.

    Here is the call chain:

    image

    image

    image

  15. alexeyzimarev commented on Mar 6, 2024

    @alexeyzimarev
    Member

    It's always possible to use one client instance to do the first call, and then use the result of that call on the regular instance. The first instance gets disposed after making that single call. The Twitter authenticator example I made a while ago does exactly that.

    The problem of any property being assignable after the client instance is created, whilst it is being used is that it by definition makes the client not thread-safe. Someone might just replace the authenticator for their single call. Meanwhile the other thread does the call expecting the original authenticator, and it all becomes a mess.

  16. rassilon commented on Mar 7, 2024

    @rassilon

    It seems to me that you could write an aggregated Authenticator by taking something similar to my updated authenticator sample authenticator and adding an overridden Authenticate method and determine what authenticator actually to use based on the contents of the RestRequest, but that might be too much fragility or pain and suffering... However, that'd be up to you to decide...

  17. rassilon commented on Mar 12, 2024

    @rassilon

    Additionally, the RestSharp request object has an Authenticator property that you can override the RestClient Options Authenticator property value for any request that needs to use a differing Authenticator from the default.

  18. abc123xxx commented on Mar 13, 2024

    @abc123xxx

    ok, the request authenticator is the solution. Thank you for the tip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions