Skip to content

C# GenericHost remove state from TokenProvider abstract base class so it makes more sense for JIT requested tokens (for long lived ApiClients with OAuth security schemes)#22233

Merged
wing328 merged 1 commit intoOpenAPITools:masterfrom
calcasa:update-token-provider
Jan 22, 2026
Merged

C# GenericHost remove state from TokenProvider abstract base class so it makes more sense for JIT requested tokens (for long lived ApiClients with OAuth security schemes)#22233
wing328 merged 1 commit intoOpenAPITools:masterfrom
calcasa:update-token-provider

Conversation

@EraYaN
Copy link
Copy Markdown
Contributor

@EraYaN EraYaN commented Oct 27, 2025

Split 2 of #22062

Make TokenProvider not contain state so subclassing actually works correctly with JIT requested tokens (for long lived ApiClients)
This allows the ApiClient to use say client credentials and an OAuth flow to requests and cache tokens appropriately without knowing the tokens before hand. The current abstract base class has an array with pre-configured tokens in it, for OAuth authentication this is not really workable. And the TokenContainer type is really what should have the tokens. This library could provide a standard implementation for API-key type authentications, that does work with fixed keys/headers.

Honestly the TokenProvider abstract base class should probably be an interface.

Example implementation
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ApiTest.Shared;
using Calcasa.Api;
using Calcasa.Api.Client;
using Duende.IdentityModel.Client;
using Microsoft.Extensions.Options;

public class ServiceOAuthTokenProvider : TokenProvider<OAuthToken>
{
    private readonly string ClientId;
    private readonly string ClientSecret;
    private readonly string TokenUrl;
    private readonly HttpClient AuthClient;
    private ConcurrentDictionary<string, (TokenResponse Token, DateTime ExpiresOn)> Tokens;

    public ServiceOAuthTokenProvider(IOptions<CalcasaApiOptions> options)
    {
        Tokens = [];
        ClientId = options.Value.ClientId;
        ClientSecret = options.Value.ClientSecret;
        TokenUrl = options.Value.TokenUrl;
        AuthClient = new HttpClient();
    }

    public override ValueTask<OAuthToken> GetAsync(string header = "", CancellationToken cancellation = default)
    {
        var data = Tokens.GetValueOrDefault(header);

        if (data.Token != null)
        {
            if (!data.Token.IsError || data.ExpiresOn > DateTime.UtcNow)
            {
                return ValueTask.FromResult(new OAuthToken(data.Token.AccessToken));
            }
        }

        var request = new ClientCredentialsTokenRequest
        {
            Address = TokenUrl,
            ClientId = ClientId,
            ClientSecret = ClientSecret,
            ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader, // Recommended as opposed to secrets in body.
        };

        data.Token = AuthClient.RequestClientCredentialsTokenAsync(request).Result;

        if (data.Token.IsError)
        {
            throw new ApplicationException("Could not refresh token: [" + data.Token.ErrorType + "] " + data.Token.Error + "; " + data.Token.ErrorDescription);
        }
        else
        {
            data.ExpiresOn = DateTime.UtcNow.AddSeconds(data.Token.ExpiresIn);
            Tokens.AddOrUpdate(header, (h) => data, (h, oldData) => data);

            return ValueTask.FromResult(new OAuthToken(data.Token.AccessToken));
        }
    }
}

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

C# Technical Committee
@mandrean @shibayan @Blackclaws @lucamazzanti @iBicha

@devhl-labs

@EraYaN EraYaN changed the title C# GenerticHost remove state from TokenProvider abstract base class so it makes more sense for JIT requested tokens (for long lived ApiClients with OAuth security schemes) C# GenericHost remove state from TokenProvider abstract base class so it makes more sense for JIT requested tokens (for long lived ApiClients with OAuth security schemes) Oct 27, 2025
…rrectly with JIT requested tokens (for long lived ApiClients)
@EraYaN EraYaN force-pushed the update-token-provider branch from 1b5141b to 4a60f92 Compare January 22, 2026 10:06
@wing328 wing328 merged commit 29befb9 into OpenAPITools:master Jan 22, 2026
73 checks passed
@wing328
Copy link
Copy Markdown
Member

wing328 commented Jan 22, 2026

Just merged it. Thanks for the PR

My understanding is that this is not a breaking change.

In case it's, we will provide a way (option) for fallback.

@devhl-labs
Copy link
Copy Markdown
Contributor

@EraYaN Why was FullMode changed from DropWrite to DropOldest? Isn't DropWrite better so we're using the token ready for the longest first, and prevent rate limits, right?

@EraYaN
Copy link
Copy Markdown
Contributor Author

EraYaN commented Apr 7, 2026

@EraYaN Why was FullMode changed from DropWrite to DropOldest? Isn't DropWrite better so we're using the token ready for the longest first, and prevent rate limits, right?

You actually want the newest token to be saved, the oldest token is the one most "worthless", it's the most likely to be expired or have outdated claims, right? Although maybe the channel should never reach capacity anyway or I have misunderstood how the rate limiting and token caching works. At least for our service, you get one token and use that for thousands of requests until it expires and then in a JIT fashion it requests a new token.

@devhl-labs
Copy link
Copy Markdown
Contributor

devhl-labs commented Apr 11, 2026

I would think a simple rate limit provider would only concern itself with preventing throttling, so the oldest is the first to be used. Though you can always provide your own. The provider that is used is configurable. Also, perhaps different types of tokens should have a different default provider. Then we could consider expiration and claims.

@EraYaN
Copy link
Copy Markdown
Contributor Author

EraYaN commented Apr 13, 2026

Using the authentication layer for rate limiting feels gross in general. It's should just be a cache of some type of credential, be it JIT OAuth tokens or hardcoded api keys.

@devhl-labs
Copy link
Copy Markdown
Contributor

It is a cache so im not sure what you mean. Can you expand your point?

@EraYaN
Copy link
Copy Markdown
Contributor Author

EraYaN commented Apr 15, 2026

It it a cache, but it's not JUST a cache. It does more, and I always feel that rate-limting api calls is an application concern and should probably depend on server-side info (headers) anyway (and need more info than just "there is a request". The way it's done here just seems a bit shoehorned into something that should essentially be a fancy dictionary. It's not that important of course, we often just remove the file before we ship our API clients for example.

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

Labels

Client: C-Sharp Enhancement: Code Cleanup General refactoring, removal of deprecated things, commenting, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants