I have a .Net Core API app deployed to Azure App Service. There is only one endpoint, and all it does is call another API (inside MyCLass) and return the response.
I have the following in my Startup:
services.AddScoped<IOAuthService, OAuthService>();
services.AddHttpClient<IMyClass, MyClass>(client =>
{
var authConfig = config.Get<OAuthConfig>();
var oAuthService = services.BuildServiceProvider().GetRequiredService<IOAuthService>();
var token = oAuthService.GetTokenAsync(authConfig).GetAwaiter().GetResult();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
});
And here is the OAuthService.GetTokenAsync method:
private IConfidentialClientApplication app;
public async Task<string> GetTokenAsync(OAuthConfig config)
{
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithTenantId(config.TenantId)
.WithClientSecret(config.ClientSecret)
.WithLegacyCacheCompatibility(false) // No need to share with ADAL.NET; increases performance
.Build();
var scopes = new string[] { $"{config.Resource}/.default" };
var authResult = await app.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
return authResult.AccessToken;
}
A HttpClient is injected into MyClass where it calls another API and returns the response. The above code results in socket handle leaks and SNAT port exhaustion. However, changing the above code to the following would solve the problem:
// In Startup
services.AddSingleton<IOAuthService, OAuthService>();
services.AddHttpClient<IHdcApiDataConnector, HdcApiDataConnector>();
// In MyClass, whenever making the http call, make a call to `OAuthService.GetTokenAsync`
// and append the token to the request on the fly
My question is why exactly the second code solves the socket handle leak problem?
Related
I successfully used this article to Secure my WASM app and the default WeatherForecastController. What I want to do now is extend the RemoteUserAccount to add the ClientId so it can be added to the User's Claims and always accessible.
What I am just now realizing is that the WeatherForecastController authorizes without issue but if I make any other Controllers and try to call an endpoint, I always get 401.
When a user is Authenticated, I have attempted to the do the below, but it only works if I remove the [RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")] from the controller.
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
var userIdentity = (ClaimsIdentity)initialUser.Identity;
var httpClient = _httpClientFactory.CreateClient();
httpClient.BaseAddress = new Uri("https://localhost:5001/");
var httpResponseMessage = await httpClient.GetAsync($"AdUser?ADObjectId=xxxxx");
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream = await httpResponseMessage.Content.ReadAsStreamAsync();
var adUser = await JsonSerializer.DeserializeAsync<AduserModel>(contentStream);
}
foreach (var role in account.Roles)
{
userIdentity.AddClaim(new Claim("appRole", role));
}
}
I think I need to reuse the httpClient from my Program.cs but I've never done any of this before, I'm usually the UI dev.
builder.Services.AddHttpClient("BlazorWasmHostedAAD.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("BlazorWasmHostedAAD.ServerAPI"));
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, SecureUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://xxx/BlazorHostedAPI.Access");
options.UserOptions.RoleClaim = "appRole";
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, SecureUserAccount, SecureAccountFactory>();
Add HTTP client like ...
// The API URL
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
Inject HttpClient
#inject HttpClient _apiClient
To talk to your API the url will be something like
var response = await _apiClient.GetAsync("/api/hello");
...
I've built an ASP.NET core Web App calling an ASP.NET Core Web API that is secured using Azure AD as described here.
I've also built a Xamarin mobile application that authenticates to the Web App's Azure App Service and utilizes the downstream Web API as described here.
The Web App can access the API without issue; however, the Xamarin application fails with an HTTP Response Message: Unauthorized.
Utilizing the guidance from the Microsoft Identity team here, I was able to track down the cause of the failure.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
A snippet from the source code for the requirement shows where it fails. Specifically, it is on the IsAuthenticated check.
public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler<DenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement)
{
var user = context.User;
var userIsAnonymous =
user?.Identity == null ||
!user.Identities.Any(i => i.IsAuthenticated);
if (!userIsAnonymous)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
Tracing the code from the working Web App, the context is generated as shown below. The critical point is the GetAccessTokenForUserAsync method gets an access token for a downstream API on behalf of the user account for which the claims are provided in the User member of the controller's HttpContext parameter.
...
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
await PrepareAuthenticatedClient();
var response = await httpClient.GetAsync($"https://example.com/api/mycontroller");
...
private async Task PrepareAuthenticatedClient()
{
var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new[] { "api://xxxx/.default"});
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
Tracing the code from the failing Xamarin application, the context is generated as shown below.
...
PCA = PublicClientApplicationBuilder
.Create(ClientID)
.WithTenantId(TenantID)
.WithRedirectUri($"msal{ClientID}://auth")
.Build();
authResult = await App.PCA.AcquireTokenInteractive("api://xxxx/.default").ExecuteAsync();
var result = Network.GetHttpContentWithTokenAsync(authToken);
...
public async Task<string> GetHttpContentWithTokenAsync(string token)
{
HttpClient client = new HttpClient();
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/mycontroller");
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await client.SendAsync(message);
string responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
How do get the authenticated user into the HTTP context for the Xamarin application?
I have created an ASP.NET Framework application using Microsoft Identify Platform from the standard template and used ConfidentialClient to acquire an access token. I now want to use this access token to call the Azure DevOps REST API.
My scenario is:
Open the application and immediately get asked to log in
Acquire an access token from ConfidentialClient
Execute an API call to Azure DevOps (e.g. GET https://dev.azure.com/{organization}/_apis/projects)
I believe I have completed steps 1 and 2 (code below), but when I execute the API is doesn't return the results, merely a HTML page asking me to login
The access token is recovered from the following code:
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
var authCode = context.Code;
var tenantId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
var authority = aadInstance + tenantId;
//string[] scopes = new string[] { "https://graph.microsoft.com/User.Read" };
string[] scopes = new string[] { "https://app.vssps.visualstudio.com/user_impersonation" };
//string[] scopes = new string[] { "https://graph.microsoft.com/User.Read", "https://app.vssps.visualstudio.com/user_impersonation" };
// Get the access token from the ConfidentialClientApplication)
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithRedirectUri(redirectUri)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
var authResult = await app.AcquireTokenByAuthorizationCode(scopes, authCode).ExecuteAsync();
string accessToken = authResult.AccessToken;
Debug.WriteLine($"Access Token: {accessToken}");
//await GetProfileData(accessToken);
await GetProjectList(accessToken);
}
If I run this I get the access token but using this as the bearer token in my API call doesn't work. The method for calling the API is as follows:
private async Task GetProjectList(string accessToken)
{
// Get the Project List from the Azure DevOps API
var httpClient = new HttpClient();
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
"https://dev.azure.com/gp-ementris/_apis/projects");
httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", accessToken);
var response = await httpClient.SendAsync(httpRequest);
if (response.IsSuccessStatusCode)
{
Debug.WriteLine(await response.Content.ReadAsStringAsync());
}
}
Can someone help explain how I can get the API to work with the token?
Thanks
I have an Azure function that makes a http call to a webapi endpoint. I'm following this example GitHub Polly RetryPolicy so my code has a similar structure. So in Startup.cs i have:
builder.Services.AddPollyPolicies(config); // extension methods setting up Polly retry policies
builder.Services.AddHttpClient("MySender", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});
My retry policy looks like this:
public static class PollyRegistryExtensions
{
public static IPolicyRegistry<string> AddBasicRetryPolicy(this IPolicyRegistry<string> policyRegistry, IMyConfig config)
{
var retryPolicy = Policy
.Handle<Exception>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(config.ServiceRetryAttempts, retryCount => TimeSpan.FromMilliseconds(config.ServiceRetryBackOffMilliSeconds), (result, timeSpan, retryCount, context) =>
{
if (!context.TryGetLogger(out var logger)) return;
logger.LogWarning(
$"Service delivery attempt {retryCount} failed, next attempt in {timeSpan.TotalMilliseconds} ms.");
})
.WithPolicyKey(PolicyNames.BasicRetry);
policyRegistry.Add(PolicyNames.BasicRetry, retryPolicy);
return policyRegistry;
}
}
My client sender service receives IReadOnlyPolicyRegistry<string> policyRegistry and IHttpClientFactory clientFactory in its constructor. My code calling the client is the following:
var jsonContent = new StringContent(JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>(PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context($"GetSomeData-{Guid.NewGuid()}", new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(ctx =>
httpClient.SendAsync(requestMessage), context);
When I attempt to test this with no endpoint service running then for the first retry attempt, the retry handler is fired and my logger records this first attempt. However, on the second retry attempt i get a error message saying:
The request message was already sent. Cannot send the same request
message multiple times
I know that other people have encountered a similar problem (see Retrying HttpClient Unsuccessful Requests and the solution seems to be do what i'm doing (i.e. use HttpClientFactory). However, i DON'T get this problem if i define my retry policy as part of the configuration in Startup as so:
builder.Services.AddHttpClient("MyService", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromMilliseconds(1000));
}
and simply call my service as so:
var response = await httpClient.SendAsync(requestMessage);
BUT doing it this way i lose the ability to pass my logger in the retry policy context (which is the whole reason i'm injecting in IReadOnlyPolicyRegistry<string> policyRegistry - I can not do this at startup). Another benefit is for unit testing - i can simply inject in the same collection with the same policy without copying and pasting a whole bunch of code and making the unit test redundant since i'm no longer testing my service. Having the policy defined in the startup makes this impossible. So my question is, is there a way to not get this duplicate request error using this approach ?
Here's an alternative solution (which I prefer).
The PolicyHttpMessageHandler added by AddPolicyHandler will create a Polly Context if one isn't already attached. So you can add a MessageHandler that creates a Context and attaches the logger:
public sealed class LoggerProviderMessageHandler<T> : DelegatingHandler
{
private readonly ILogger _logger;
public LoggerProviderMessageHandler(ILogger<T> logger) => _logger = logger;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpClientRequestId = $"GetSomeData-{Guid.NewGuid()}";
var context = new Context(httpClientRequestId);
context[PolicyContextItems.Logger] = _logger;
request.SetPolicyExecutionContext(context);
return await base.SendAsync(request, cancellationToken);
}
}
A little extension method for registration makes it nice:
public static IHttpClientBuilder AddLoggerProvider<T>(this IHttpClientBuilder builder)
{
if (!services.Any(x => x.ServiceType == typeof(LoggerProviderMessageHandler<T>)))
services.AddTransient<LoggerProviderMessageHandler<T>>();
return builder.AddHttpMessageHandler<LoggerProviderMessageHandler<T>>();
}
And then you can use it as such (note that it must be before the AddPolicyHandler so that it creates the Context first):
builder.Services.AddHttpClient("MyService", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddLoggerProvider<MyService>()
.AddPolicyHandler(GetRetryPolicy());
At runtime, LoggerProviderMessageHandler<MyService> gets an ILogger<MyService>, creates a Polly Context containing that logger, and then invokes PolicyHttpMessageHandler, which uses the existing Polly Context, so your retry policy can successfully use context.TryGetLogger.
You are getting a bit too much caught up in Polly and how to configure it, and forgetting a few basic aspects. Don't worry, this is too easy to do!
First, you cannot send the same HttpRequestMessage more than once. See this extensive Q&A on the subject. It's also documented officially, though the documentation is a bit opaque on the reason.
Second, as you have it coded, the request you created was captured once by the lambda, and then reused over and over again.
For your particular case, I would move the creation of the request inside the lambda that you are passing to ExecuteAsync. This gives you a new request each time.
Modifying your code,
var jsonContent = new StringContent(
JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context(
$"GetSomeData-{Guid.NewGuid()}",
new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(ctx =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.SendAsync(requestMessage), context);
}
The other captures: logger, authToken, might be OK if they don't change request over request, but you may need to move other variables inside the lambda as well.
Not using Polly at all makes most of this thought process unnecessary, but with Polly, you have to remember retries and policy are occurring across time and context.
I'm trying to work with a application to use IdentityServer4, it has the basic setup of the identity server, MVC client, and web API.
I have a custom Profile service (which I've registered in Startup.cs) where I'm adding a custom claim, here's my GetProfileDataAsync method:
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
new Claim("TestFullName", user.FullName),
};
context.IssuedClaims.AddRange(claims);
return Task.FromResult(0);
}
My problem is that when I log into the identity server, I can see the additional claim - but when I call my API from the MVC app, my custom claim isn't there. Here's the code in my MVC app to call the API:
public async Task<IActionResult> ClientAuthorizedAPICall(string token)
{
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("testAPI");
// call api
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:5001/identity");
...
}
And the method on my API is simply:
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
Am I doing something wrong? Or should I be doing something different instead of using User.Claims?
Like rawel's comment says, you'll want to use the MVC app user's access token to make your API call. It would look something like this:
// get the current user's access token
var accessToken = await HttpContext.GetTokenAsync("access_token");
// call api
var client = new HttpClient();
client.SetBearerToken(accessToken);
var response = await client.GetAsync("http://localhost:5001/identity");
You can see a similar approach in the quickstart on hybrid flow.
To get your custom user claim into the access token for your API, you'll need to include it when defining the API resource. For example:
new ApiResource("testAPI", "Test API", new[] { "TestFullName" }),