OAuth & Token Extension
OAuth & Token Extension
The WebApiClientCore.Extensions.OAuths extension makes it easy to support token acquisition, refresh, and application.
Objects and Concepts
ITokenProviderFactory
A factory for creating ITokenProvider instances, providing the ability to get or create an ITokenProvider based on the HttpApi interface type.
ITokenProvider
A token provider used to obtain tokens. It triggers a token re-request or refresh on the first request after the token expires.
Since v2.1.6, ITokenProvider supports cancellation tokens: GetTokenAsync and ClearTokenAsync provide overloads accepting a CancellationToken, so token requests and refreshes can be cancelled together with the business request, avoiding token acquisition for requests that have already been abandoned.
OAuthTokenAttribute
An attribute for applying tokens. It creates an ITokenProvider using ITokenProviderFactory, then uses the ITokenProvider to obtain the token, and finally applies the token to the request message.
OAuthTokenHandler
An HTTP message handler that functions similarly to OAuthTokenAttribute. Additionally, if the server returns an unauthorized response (401 status code) for unexpected reasons, it discards the old token, requests a new one, and retries the request.
OAuth Client Mode
Register TokenProvider for the Interface
// Register and configure the tokenProvider for the interface in client mode
services.AddClientCredentialsTokenProvider<IUserApi>(o =>
{
o.Endpoint = new Uri("http://localhost:6000/api/tokens");
o.Credentials.Client_id = "clientId";
o.Credentials.Client_secret = "xxyyzz";
});Applying the Token
Using OAuthTokenAttribute
OAuthTokenAttribute belongs to the WebApiClientCore framework layer, making it easy to manipulate request content and response models. For example, you can conveniently add the token as a form field to an existing request form, or read the corresponding business model after deserializing the response message. However, it cannot retry requests within the same call if the token becomes invalid after the server issues it. Using OAuthTokenAttribute will result in a failed request that cannot be avoided.
/// <summary>
/// User operation interface
/// </summary>
[OAuthToken]
public interface IUserApi
{
...
}The default implementation of OAuthTokenAttribute places the token in the Authorization request header. If your interface needs to place the token elsewhere, such as in the URI query string, you need to override OAuthTokenAttribute:
public class UriQueryTokenAttribute : OAuthTokenAttribute
{
protected override void UseTokenResult(ApiRequestContext context, TokenResult tokenResult)
{
context.HttpContext.RequestMessage.AddUrlQuery("mytoken", tokenResult.Access_token);
}
}
[UriQueryToken]
public interface IUserApi
{
...
}Using OAuthTokenHandler
The strength of OAuthTokenHandler is its support for multiple retry attempts within a single request. If the server returns a 401 status code after issuing the token, OAuthTokenHandler discards the old token, requests a new one, and retries the request within the same call, making it appear as a normal request. However, OAuthTokenHandler is not part of the WebApiClientCore framework layer. It can only access the original HttpRequestMessage and HttpResponseMessage. Appending the token to the HttpRequestMessage.Content is difficult. Similarly, using a field from HttpResponseMessage.Content corresponding to the business model as the basis for determining token validity instead of HTTP status codes (such as 401) is also challenging.
// Register OAuthTokenHandler when registering the interface
services
.AddHttpApi<IUserApi>()
.AddOAuthTokenHandler();The default implementation of OAuthTokenHandler places the token in the Authorization request header. If your interface needs to place the token elsewhere, such as in the URI query string, you need to override OAuthTokenHandler:
public class UriQueryOAuthTokenHandler : OAuthTokenHandler
{
/// <summary>
/// HTTP message handler for applying tokens
/// </summary>
/// <param name="tokenProvider">Token provider</param>
public UriQueryOAuthTokenHandler(ITokenProvider tokenProvider)
: base(tokenProvider)
{
}
/// <summary>
/// Apply the token
/// </summary>
/// <param name="request"></param>
/// <param name="tokenResult"></param>
protected override void UseTokenResult(HttpRequestMessage request, TokenResult tokenResult)
{
// var builder = new UriBuilder(request.RequestUri);
// builder.Query += "mytoken=" + Uri.EscapeDataString(tokenResult.Access_token);
// request.RequestUri = builder.Uri;
var uriValue = new UriValue(request.RequestUri);
uriValue = uriValue.AddQuery("myToken", tokenResult.Access_token);
request.RequestUri = uriValue.ToUri();
}
}
// Register UriQueryOAuthTokenHandler when registering the interface
services
.AddHttpApi<IUserApi>()
.AddOAuthTokenHandler((s, tp) => new UriQueryOAuthTokenHandler(tp));TokenProvider Shared by Multiple Interfaces
You can define a base interface for HTTP interfaces and configure the TokenProvider for the base interface. For example, the IBaidu_XXX_Api and IBaidu_YYY_Api interfaces below both inherit from IBaidu, so you only need to configure the TokenProvider for IBaidu.
[OAuthToken]
public interface IBaidu
{
}
public interface IBaidu_XXX_Api : IBaidu
{
[HttpGet]
Task xxxAsync();
}
public interface IBaidu_YYY_Api : IBaidu
{
[HttpGet]
Task yyyAsync();
}// Register and configure the token provider for password mode
services.AddPasswordCredentialsTokenProvider<IBaidu>(o =>
{
o.Endpoint = new Uri("http://localhost:5000/api/tokens");
o.Credentials.Client_id = "clientId";
o.Credentials.Client_secret = "xxyyzz";
o.Credentials.Username = "username";
o.Credentials.Password = "password";
});Custom TokenProvider
The extension package includes two standard token request modes: OAuth Client and Password. However, many interface providers only loosely follow the OAuth spirit in their implementation. In such cases, you need to customize the TokenProvider. Assuming the interface provider's token request interface is as follows:
public interface ITokenApi
{
[HttpPost("http://xxx.com/token")]
Task<TokenResult> RequestTokenAsync([Parameter(Kind.Form)] string clientId, [Parameter(Kind.Form)] string clientSecret, CancellationToken cancellationToken = default);
}Delegate TokenProvider
Delegate TokenProvider is the simplest implementation. It uses a delegate for requesting the token as the implementation logic of the custom TokenProvider. Prefer the overload with a CancellationToken parameter (v2.1.6+) to forward the cancellation token to the token endpoint:
// Register a custom tokenProvider for the interface (v2.1.6+ recommended)
services.AddTokenProvider<IUserApi>((s, cancellationToken) =>
{
return s.GetRequiredService<ITokenApi>().RequestTokenAsync("id", "secret", cancellationToken);
});The old overload without the CancellationToken parameter still works, but it cannot interrupt the token acquisition when the request is cancelled:
// Old style: still compatible
services.AddTokenProvider<IUserApi>(s =>
{
return s.GetRequiredService<ITokenApi>().RequestTokenAsync("id", "secret");
});Full Implementation of TokenProvider
// Register CustomTokenProvider for the interface
services.AddTokenProvider<IUserApi, CustomTokenProvider>();public class CustomTokenProvider : TokenProvider
{
public CustomTokenProvider(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
protected override Task<TokenResult> RequestTokenAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
return serviceProvider.GetRequiredService<ITokenApi>().RequestTokenAsync("id", "secret", cancellationToken);
}
protected override Task<TokenResult> RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token, CancellationToken cancellationToken)
{
return this.RequestTokenAsync(serviceProvider, cancellationToken);
}
}Pre-v2.1.6 overrides are obsolete
Before v2.1.6, you overrode RequestTokenAsync(IServiceProvider) and RefreshTokenAsync(IServiceProvider, string) without the CancellationToken parameter. These methods are now marked [Obsolete]: they still compile but produce compiler warnings. Please migrate to the new signatures with the CancellationToken parameter.
Options for Custom TokenProvider
Each TokenProvider has a Name property, which is the same value as the Name of the ITokenProviderBuilder returned by service.AddTokenProvider(). You can use the GetOptionsValue() method of TokenProvider to read the Options value, and configure the Options using the Name of ITokenProviderBuilder.
Advanced Configuration
Token Refresh Window
To avoid token expiration during use, you can configure the refresh window:
services.AddHttpApi<IUserApi>()
.ConfigureOAuthTokenOptions(o =>
{
o.UseTokenRefreshWindow = true;
o.RefreshWindowSeconds = 120; // Refresh 2 minutes early
// Or use percentage calculation
// o.RefreshWindowPercentage = 0.1; // Refresh 10% of time early
});Disable refresh_token
If the server does not support refresh_token, you can disable it:
services.AddClientCredentialsTokenProvider<IUserApi>(o =>
{
o.Endpoint = new Uri("http://localhost:6000/api/tokens");
o.Credentials.Client_id = "clientId";
o.Credentials.Client_secret = "xxyyzz";
o.UseRefreshToken = false; // Disable refresh_token, re-request when expired
});Alias Functionality
Support for registering multiple TokenProviders with aliases:
// Register multiple TokenProvider aliases
services.AddClientCredentialsTokenProvider<IUserApi>("api1", o =>
{
o.Endpoint = new Uri("http://localhost:6000/api/tokens1");
// ...
});
services.AddClientCredentialsTokenProvider<IUserApi>("api2", o =>
{
o.Endpoint = new Uri("http://localhost:6000/api/tokens2");
// ...
});
// Dynamic selection via parameter
public interface IUserApi
{
[OAuthToken("providerName")]
Task GetDataAsync(string providerName);
}TokenProvider Search Mode
The TokenProviderSearchMode property of OAuthTokenAttribute controls how to find the TokenProvider:
[OAuthToken(TokenProviderSearchMode = TypeMatchMode.Exact)]
public interface IUserApi
{
// ...
}TypeOrBaseTypes(default): Search from the current interface type, then parent interfacesExact: Only search for the current interface type