I just found out about the Refit library on GitHub (link: https://github.com/reactiveui/refit) . Besides my first steps in this huge world, I tried to understand why the use of this library comes handy instead of the use of the usual HttpClient when we need to make http calls towards, for example, an API service. By reading around I understood the reason that creating the httpClient by ourselves, setting the headers and other configurations, is too old style and low-level. That's where Refit takes place. I then tried to make one step forward and read about the authentication part. I noticed, according to the github page of the library, that in order to make authentication work, we need to deal again with the HttpClient that we finally managed to get rid off. The example shown on the official page is:
class AuthenticatedHttpClientHandler : HttpClientHandler
{
private readonly Func<Task<string>> getToken;
public AuthenticatedHttpClientHandler(Func<Task<string>> getToken)
{
if (getToken == null) throw new ArgumentNullException("getToken");
this.getToken = getToken;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// See if the request has an authorize header
var auth = request.Headers.Authorization;
if (auth != null)
{
var token = await getToken().ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token);
}
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
class LoginViewModel
{
AuthenticationContext context = new AuthenticationContext(...);
private async Task<string> GetToken()
{
// The AcquireTokenAsync call will prompt with a UI if necessary
// Or otherwise silently use a refresh token to return
// a valid access token
var token = await context.AcquireTokenAsync("http://my.service.uri/app", "clientId", new Uri("callback://complete"));
return token;
}
public async void LoginAndCallApi()
{
var api = RestService.For<IMyRestService>(new HttpClient(new AuthenticatedHttpClientHandler(GetToken)) { BaseAddress = new Uri("https://the.end.point/") });
var location = await api.GetLocationOfRebelBase();
}
}
I am wondering what concept am I missing here. The purpose of the library is to use more high level code, setting interfaces that are enough to call an API service. This purpose is achieved before the authentication part because all the Http settings and so on are made on purpose under the hood. But as soon as we step in this field we find again HttpHandlers, HttpRequestMessages and HttpClients losing what's the purpose of the library itself. Can someone explain me please what am I missing in the bigger picture? thanks in advance
I've been trying to figure out authentication myself, here are my own observations in using Refit.
TL;DR: there are alternatives to set the authentication that do not require using the HttpClient, observations 2 and 3 below.
There are at least three way to handle authentication:
1) As noted in the GitHub page, you can pass in an HttpClient with an HttpClientHandler and in the handler set the Authorization header. In terms of why you need to use the handler, I have noticed that Refit will set the Authorization header to whatever value is specified in the attribute before making the HTTP request, if you set the header in the HttpClient prior to creating the Refit instance it will not work, for example this won't work:
[Get("/secretStuff")]
[Headers("Authorization: Bearer")]
Task<Location> GetLocationOfRebelBase();
. . .
var client = new HttpClient() { BaseAddress = new Uri("https://the.end.point/") };
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", someToken);
var api = RestService.For<IMyRestService>(client);
var location = await api.GetLocationOfRebelBase();
The Authorization header will be "Authorization: Bearer", the token will not be there. You need to alter the HttpClient just prior to the HTTP request is made, in the HttpClientHandler (or DelgatingHandler).
2) When creating a new instance of the Refit api client, pass in the base address to RestService.For rather than an HttpClient and specify the AuthorizationHeaderValueGetter, e.g.:
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com", new RefitSettings {
AuthorizationHeaderValueGetter = () => {
var token = SomeMethodToGetAToken();
Task.FromResult(token);
}
});
3) Pass the token into the api method, e.g.:
[Get("/users/{user}")]
Task<User> GetUser(string user, [Header("Authorization")] string authorization);
This is mentioned in the Refit GitHub page: https://github.com/reactiveui/refit#dynamic-headers.
Related
I'm developing a Blazor App which consumes three different APIs, and everything is going fine until the API token expires.
My problem is about having the token with the HTTPClientFactory. Right now, I'm using this approach but feel free to tell me I'm completely wrong about it
services.AddHttpClient("first-api", options =>
{
options.BaseAddress = new Uri(first_api_uri);
});
First, I inject the HttpClient for any of the APIs. Let´s say it´s the first one.
After that, I configure the HttpClientFactory to my APIService and set the Default Header (bearer token) reading from my Protected Local Storage.
services.AddScoped<IAPIOneService, APIOneService>(context =>
{
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("first-api");
var storage = context.GetRequiredService<ProtectedLocalStorage>();
AddHeader(httpClient, storage);
var apiClient = new APIClient(httpClient);
return new APIOneService(apiClient);
});
As I said before, the AddHeader set the Authorization Header to the HttpClient, which goes inside of a "handler" of requests (built by me), called APIClient.
If the token expires, I get a 401 from the server and send the user to the login page, but, when it logs in again, the HttpClient keeps the old token, probably because it does not pass through the DI section where the AddHeader writes the new Authentication Header.
I'm probably doing it wrong, but writing a HttpMessageHandler just does not work because AddHeader cannot read from my Protected Local Storage. It's something from Blazor rules.
Can someone help me and give me the right way to do it?
You can create a custom TokenRefreshHandler and add it to the HttpClient pipeline. The handler could check the response status code for each request and determine if it is a 401 Unauthorized error. If so, it can refresh the token and add this new token to the header before re-sending the request.
TokenRefreshHandler.cs
public class TokenRefreshHandler : DelegatingHandler
{
private readonly ProtectedLocalStorage _protectedLocalStorage;
public TokenRefreshHandler(ProtectedLocalStorage protectedLocalStorage)
{
_protectedLocalStorage = protectedLocalStorage;
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Refresh the token
string newToken = await RefreshToken();
_protectedLocalStorage.Set("token", newToken);
// Add the new token to the header
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
// Re-send the request with the new token
response = await base.SendAsync(request, cancellationToken);
}
return response;
}
private async Task<string> RefreshToken()
{
// Refresh the token and return the new token value
// ...
}
}
Register
services.AddHttpClient("first-api", options =>
{
options.BaseAddress = new Uri(first_api_uri);
})
.AddHttpMessageHandler<TokenRefreshHandler>();
I am looking for some advice on how best to handle the authentication when calling an API from a web app.
I have inherited an blazor .net core project, that is fundamentally a UI for API calls, to display data to the user.
When the user logs on, the application makes a call to the token endpoint of the API, and if it is a success, it proceeds, and it stores the token in a class that the pages can access, and this token is then passed down the layers, so it can be included in the header for the API call.
I need to make some changes to this app, and I am not a fan of how this is currently handled. I would ideally like to have middleware, that adds the token to each call, so the code in the service layer is much cleaner, but, the problem I have is, I would either need to cache the token globally (is that secure?) or, the username and password, so I can get a token and add it to each call.
Does anyone have any advice for a project that is set up like this? I have never had to deal with users having different permissions based on the token, so have been able to keep creds in config, and pull them out in the middleware to get the token for each call.
Obviously security is paramount here, I don't really want to expose credentials or the token itself.
I implemented adding token to each api call through message handler (in Blazor WASM):
in Program.cs/Startup
builder.Services.AddHttpClient<YourHttpClient>().ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(apiUrl);
}).AddHttpMessageHandler<CustomMessageHandler>();
CustomMessageHandler
public class CustomMessageHandler : DelegatingHandler
{
private AuthenticationStateProvider provider;
private readonly ILocalStoragee localStorage;
private readonly NavigationManager navigation;
public CustomMessageHandler(AuthenticationStateProvider authenticationStateProvider,
ILocalStorage localStorage,
NavigationManager navigationManager)
{
provider = authenticationStateProvider ?? throw new ArgumentNullException(nameof(authenticationStateProvider));
localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage));
navigation = navigation ?? throw new ArgumentNullException(nameof(navigation));
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await localStorage.GetItemAsync<string>("token");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
/// other headers
var response = await base.SendAsync(request, cancellationToken);
/// handle common status codes
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
((CustomAuthenticationStateProvider)provider).MarkUserAsLoggedOut();
navigation.NavigateTo("login");
return default;
}
return response;
}
}
I'm using a C# Refit client to have my services talk to one another via http.
I'm trying to send a Bearer token through the Authorization header, but according to the error message, it's not setting the AZ header in the request (see bottom). I've tried setting it through providing all headers, and using the [Authorize] attribute, and all the other methods they describe in their readme.
Here is my Refit client api call definition:
[Post(PresentationsBasePath + "/{presentationId}/cart")]
Task AddItemToCartAsync(long presentationId, ShoppingCartItemView item, [HeaderCollection] IDictionary<string, string> headers);
//calling it here:
await _api.AddItemToCartAsync(presentationId, item, GetTokenHeader(presentationId, token));
private Dictionary<string, string> GetTokenHeader(long presentationId, string token) => new()
{
["pres_id"] = presentationId.ToString(),
[HeaderNames.Authorization] = $"Bearer {token}",
};
However, I'm getting a 401, and looking at the Refit.ApiException that's thrown, the RequestMessage.Headers does not contain the Authorization header.
Here's how I'm registering my refit api IPresentationsApi. I'm not doing anything relating to auth in the DI configuration
var refitSettings = GetRefitSettings();
void Configure<T>() where T : class => services
.AddRefitClient<T>()
.ConfigureHttpClient(ConfigureHttpClient);
Configure<IMarsPresentationApi>();
//other apis configured below
private static void ConfigureHttpClient(IServiceProvider sp, HttpClient client)
{
var config = sp.GetRequiredService<IMarsConfiguration>();
if (config.BaseUrl == null)
throw new InvalidOperationException("Mars:BaseUrl must be configured");
client.BaseAddress = new Uri(config.BaseUrl);
}
Error shown here- you can see I get 401, and AZ header is not set in the request:
What am I doing wrong? How do I get it to send the AZ header?
first try:
services.AddRefitClient<T>(new RefitSettings { AuthorizationHeaderValueGetter = () => Task.FromResult("TestToken") }).ConfigureHttpClient(ConfigureHttpClient);
second try:
services.AddRefitClient<T>().ConfigureHttpClient(ConfigureHttpClient).AddHttpMessageHandler<AuthorizationMessageHandler>();
where:
public class AuthorizationMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancelToken)
{
HttpRequestHeaders headers = request.Headers;
AuthenticationHeaderValue authHeader = headers.Authorization;
if (authHeader != null)
headers.Authorization = new AuthenticationHeaderValue(authHeader.Scheme, "TestToken");
return await base.SendAsync(request, cancelToken);
}
}
This discussion continued on a GitHub issue for Refit. Somebody usefully pointed out that by default Authorization headers get stripped off when HttpClient follows a redirect - https://github.com/reactiveui/refit/issues/1374#issuecomment-1212776451.
I also hit the same issue described. After reading the suggestion in the GitHub issue discussion I configured my HttpClient instance that disabled auto redirects.
var httpClient = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = false })
This confirmed that the server API was returning 301-Moved Permanently responses to API endpoints that looked correct from the API documentation I had been given.
Looking at how I had configured my API endpoints they were configured like /{presentationId}/cart without slash at the end.
I updated the endpoints to end with a final slash /{presentationId}/cart/ and this stopped the server redirects being returned.
I need to call a Http Azure Function from another Azure Function.
At present, I call an Azure Key Vault to get the target Function's Key, and put that in the URL as documented here: https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=csharp#api-key-authorization
However, I want to start using a Managed Identity and DefaultAzureCredential but I cannot find out how to use DefaultAzureCredential with HttpClient or similar.
How could I use DefaultAzureCredential and HttpClient to call a Function from another Function?
The simplistic way of solving this issue is like this:
var targetFunctionAppAppRegistrationApplicationId = "A Guid that you must get from your target Function's Authentication configuration - 'App (client) ID'";
var url = "https://yourfunctionappname.azurewebsites.net/api/targetfunctionname";
var creds = new DefaultAzureCredential();
var token = await creds.GetTokenAsync(new Azure.Core.TokenRequestContext(new[] { targetFunctionAppAppRegistrationApplicationId }));
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
var result = await client.GetAsync(url);
// Anything else you want to do with the result
}
Credits for the above to https://spblog.net/post/2021/09/28/call-azure-ad-secured-azure-function-from-logic-app-or-another-function-with-managed-identity
However
The code above will soon cause socket exhaustion. The correct way is to use HttpClientFactory, as explained here: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
Since this specific use case is not covered in those docs, below is an example of how it would look like.
First, you need a MessageHandler:
public class AzureDefaultCredentialsAuthorizationMessageHandler : DelegatingHandler
{
private readonly TokenRequestContext TokenRequestContext;
private readonly DefaultAzureCredential Credentials;
public AzureDefaultCredentialsAuthorizationMessageHandler()
{
// This parameter is actually a list of scopes.
// If your target Function has defined scopes then you should use them here.
// TokenRequestContext also supports many other options you should probably check out.
TokenRequestContext = new (new[] { "targetFunctionAppAppRegistrationApplicationId" });
Credentials = new DefaultAzureCredential();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var tokenResult = await Credentials.GetTokenAsync(TokenRequestContext, cancellationToken);
var authorizationHeader = new AuthenticationHeaderValue("Bearer", tokenResult.Token);
request.Headers.Authorization = authorizationHeader;
return await base.SendAsync(request, cancellationToken);
}
}
You then need to register an HttpClient with this message handler in your Dependency Injection container. If you're using the standard IServiceCollection:
services
.AddScoped<AzureDefaultCredentialsAuthorizationMessageHandler>()
.AddHttpClient<YourClassUsingTheHttpClient>((serviceProvider, httpClient) =>
{
httpClient.BaseAddress = "https://yourfunctionappname.azurewebsites.net/api/targetfunctionname";
}).AddHttpMessageHandler<AzureDefaultCredentialsAuthorizationMessageHandler>();
Finally, just have a YourClassUsingTheHttpClient class that takes an HttpClient in its constructor:
public class YourClassUsingTheHttpClient
{
public YourClassUsingTheHttpClient(HttpClient httpClient) { ... }
}
Notes
It should be noted that the code above does not deal with other important concerns like:
Error handling
Token caching
Ability to have different HttpClients and MessageHandlers for different API endpoints.
Error handler should be straightforward to add. The rest go beyond the scope of this question.
I created an interface to call the Prestashop API using Refit, for a Bot App I'm developing.
In order to call the API you need to authenticate with a Prestashop API Key, which I have. To query using the browser I only need to call an url with this format:
$"https://{ApiKey}#{mypage}.com/api"
And it authenticates using the Api Key specified before the # sign. To define the Refit HttpClient i use this code in the Startup.cs:
// This is the ApiUrl from the appsettings.json file
var apiUrl = Configuration.GetSection("PrestashopSettings").GetSection("ApiUrl").Value;
// We add the Api and specify the de/serialization will be XML
services.AddRefitClient<IPrestashopApi>(
new RefitSettings
{
ContentSerializer = new XmlContentSerializer()
})
.ConfigureHttpClient(c => c.BaseAddress = new System.Uri(apiUrl));
Then I inject the API to one of my classes and call one of its functions. The URL seems correct, if i paste the complete URL (the base + the [Get] url) to a browser it returns an XML correctly. But when I do it from the App it returns an exception:
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter:Error: Exception caught : Refit.ApiException: Response status code does not indicate success: 401 (Unauthorized).
at Refit.RequestBuilderImplementation.<>c__DisplayClass14_0`2.<<BuildCancellableTaskFuncForMethod>b__0>d.MoveNext() in D:\a\1\s\Refit\RequestBuilderImplementation.cs:line 274
--- End of stack trace from previous location where exception was thrown ---
What is the correct way of Authenticating using Refit's HttpClient? Am I doing something wrong?
UPDATE:
So I tried this:
public class HttpAuthentication : HttpClientHandler
{
private readonly string Token;
public HttpAuthentication(string token)
{
Token = token ?? throw new ArgumentException(nameof(token));
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = Token;
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
And this code in my Startup.cs:
var apiKey = Configuration.GetSection("PrestashopSettings").GetSection("ApiKey").Value;
var storeUrl = Configuration.GetSection("PrestashopSettings").GetSection("StoreUrl").Value;
// We add the Api and specify the de/serialization will be XML, and we specify the Authentication Client.
services.AddRefitClient<IPrestashopApi>(
new RefitSettings
{
ContentSerializer = new XmlContentSerializer()
})
.ConfigureHttpClient((c) => c.BaseAddress = new System.Uri(storeUrl))
.ConfigureHttpMessageHandlerBuilder((c) => new HttpAuthentication(apiKey));
And I'm still getting the same error message.
create class like this :
public class AuthenticatedHttp : HttpClientHandler
{
private readonly string Token;
public AuthenticatedHttp(string token)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
this.Token = token;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// See if the request has an authorize header
var token = this.Token;
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
and send Token To this class:
var token = await GetAccessToken();
var RestReq = RestService.For<IPerson>(new HttpClient(new AuthenticatedHttp(token)) { BaseAddress = new Uri(Url) });
Okay I figured it out at the end.
First of all I want to point out there's two solutions around this.
First Solution
You can actually authenticate using your API Key as a Request parameter, its key is ws_key, so you can send a call like this:
"https://api.yourapiaddress.com/yourentity?ws_key={API KEY HERE}"
Second solution
This is the one I opted for, just adding a Header Parameter. Found out Prestashop API 1.7 uses Basic Authorization with the API Key as username and a blank password so I built the header like this in Startup.cs:
// Encode your Api Key
String encoded = Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(apiKey));
// Add the API to DI in Dialog classes
services.AddRefitClient<IPrestashopApi>(
new RefitSettings
{
ContentSerializer = new XmlContentSerializer()
})
.ConfigureHttpClient((c) => c.BaseAddress = new Uri(storeUrl))
.ConfigureHttpClient((c) => c.DefaultRequestHeaders.Add("Authorization", "Basic " + encoded));
I used the ConfigureHttpClient function of Retrofit but you could actually achieve the same by creating your own HttpClient object and configuring the DefaultRequestHeaders like this.