Using PublicClientApplicationBuilder and AcquireTokenSilent for chaining Web API calls - asp.net-core

I'm working on an AD proof of concept using a console application and PublicClientApplicationBuilder to call Web API A and to call Web API B which also calls Web API A. (API A is just the "Weather" example, and API B just wraps API A.)
My call in API B to HttpContext.VerifyUserHasAnyAcceptedScope(ApiAyeScopes.AccessAsUser) keeps throwing:
IDW10203: The 'scope' or 'scp' claim does not contain scopes 'api://A0000000-1111-2222-3333-444444444444/access_as_user' or was not found.`
How can I resolve this and get the call from API B to API A to work?
I have the direct call to Web API A working. Here's how I authenticate:
static Boolean Authenticate()
{
// See the answer to https://social.msdn.microsoft.com/Forums/en-US/d4b2aff3-eeb1-4204-82ed-ca80232c2523/error-aadsts50076-due-to-a-configuration-change-made-by-your-administrator-or-because-you-moved-to?forum=WindowsAzureAD.
__identityApplication =
__identityApplication
?? PublicClientApplicationBuilder
.Create("000000-1111-2222-3333-444444444444")
.WithAuthority("https://login.microsoftonline.com/me.org/v2.0")
.WithRedirectUri("http://localhost:11596")
.Build();
string[] scopes = new string[] { "api://A0000000-1111-2222-3333-444444444444/access_as_user" };
__authenticationResult =
__identityApplication
.AcquireTokenInteractive(scopes)
.WithExtraScopesToConsent(new String[] { "api://B0000000-1111-2222-3333-444444444444/access_as_user" })
.WithUseEmbeddedWebView(false)
.ExecuteAsync()
.Result;
Console.WriteLine("Logged in as {0}.", __authenticationResult.Account.Username);
return null != __authenticationResult;
}
Here's how I call Web API A from the console, which works:
static List<WeatherForecast> GetWeatherForecast()
{
HttpClient httpClient = new HttpClient();
httpClient.Timeout = Timeout.InfiniteTimeSpan;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, __authenticationResult.AccessToken);
var response = httpClient.GetAsync("https://localhost:1001/weatherforecast").Result;
var jsonString = response.Content.ReadAsStringAsync().Result;
return Newtonsoft.Json.JsonConvert.DeserializeObject<List<WeatherForecast>>(jsonString);
}
Here's how I call Web API B, which partially works:
static List<WeatherForecast> GetAugmentedWeatherForecast()
{
string[] scopes = new string[] { "api://B0000000-1111-2222-3333-444444444444/access_as_user" };
var apiBeeAuthenticationResult =
__identityApplication
.AcquireTokenSilent(scopes, __authenticationResult.Account)
.ExecuteAsync()
.Result;
HttpClient httpClient = new HttpClient();
httpClient.Timeout = Timeout.InfiniteTimeSpan;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, apiBeeAuthenticationResult.AccessToken);
var response = httpClient.GetAsync("https://localhost:1101/weatherforecast").Result;
var jsonString = response.Content.ReadAsStringAsync().Result;
return Newtonsoft.Json.JsonConvert.DeserializeObject<List<WeatherForecast>>(jsonString);
}
In Web API B, I have the following:
public class ApiAyeScopes
{
public const String WeatherRead = "api://A0000000-1111-2222-3333-444444444444/ReadWeather";
public const String AccessAsUser = "api://A0000000-1111-2222-3333-444444444444/access_as_user";
}
[AuthorizeForScopes(Scopes = new[] { ApiAyeScopes.AccessAsUser })]
[Authorize(Policy = ApiBeeAuthorizationPolicies.AssignmentToReadAugmentedWeatherRequired)]
[HttpGet]
public async Task<IEnumerable<AugmentedWeatherForecast>> Get()
{
var apiAyeScopes = new String[] { ApiAyeScopes.AccessAsUser };
// See https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-web-api-call-api-acquire-token?tabs=aspnetcore#code-in-the-controller
HttpContext.VerifyUserHasAnyAcceptedScope(apiAyeScopes);
var originalResult = await _apiAyeClient.GetWeatherForecasts();
return originalResult.Select(wf => new AugmentedWeatherForecast(wf));
}
The code to get the access token is:
String accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new String[] { ApiAyeScopes.WeatherRead });

It looks like you are looking to resolve your code from API B to API A to work and API B and API A. On-Behalf-Of flow (OBO) serves the use case where an application invokes a service/web API, which in turn needs to call another service/web API.
Learn more here:
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/on-behalf-of
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow
The OBO flow is represented by the steps that follow, which are illustrated in the diagram below.
More guidance can be found here: https://github.com/Azure-Samples/ms-identity-aspnet-webapi-onbehalfof

Related

Efficiently working with On-Behalf Of access tokens in an ASP.NET Core application

Note: this is a follow-up of Reusing a Polly retrial policy for multiple Refit endpoints without explicitly managing the HttpClient
When making Refit work with Polly and an Azure AD-based authentication (On Behalf Of flow), I realized that acquiring an OBO token can be very slow (>400ms). The code for acquiring an OBO token based on the current logger in the user access token is shown below:
public async Task<string> GetAccessToken(CancellationToken token)
{
var adSettings = _azureAdOptions.Value;
string[] scopes = new string[] { "https://foo.test.com/access_as_user" };
string? httpAccessToken = _httpContextAccessor.HttpContext?.Request?.Headers[HeaderNames.Authorization]
.ToString()
?.Replace("Bearer ", "");
if (httpAccessToken == null)
throw new ArgumentNullException("Failed to generate access token (OBO flow)");
string cacheKey = "OboToken_" + httpAccessToken;
string oboToken = await _cache.GetOrAddAsync(cacheKey, async () =>
{
IConfidentialClientApplication cca = GetConfidentialClientApplication(adSettings);
var assertion = new UserAssertion(httpAccessToken);
var result = await cca.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync(token);
return result.AccessToken;
},
new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(TokenCacheExpirationInMinutes) });
return oboToken;
}
private IConfidentialClientApplication GetConfidentialClientApplication(AzureAdAuthOptions adSettings)
{
var certMetadata = _azureAdOptions.Value.ClientCertificates[0];
string certPath = certMetadata.CertificateDiskPath;
_logger.LogInformation($"GetAccessToken certificate path = {certPath}");
string certPassword = certMetadata.CertificatePassword;
var certificate = new X509Certificate2(certPath, certPassword);
_logger.LogInformation($"GetAccessToken certificate = {certificate}");
var cca = ConfidentialClientApplicationBuilder
.Create(adSettings.ClientId)
.WithTenantId(adSettings.TenantId)
.WithCertificate(certificate)
// .WithClientSecret(adSettings.ClientSecret)
.Build();
return cca;
}
This seems to work fine (not tested in a production environment though). however, I feel that I am reinventing the wheel here as I managing the OBO token caching myself.
Currently, this flow is used by Refit configuration:
private static IServiceCollection ConfigureResilience(this IServiceCollection services)
{
services
.AddRefitClient(typeof(IBarIntegration), (sp) =>
{
var accessTokenHelperService = sp.GetRequiredService<IAccessTokenHelperService>();
return new RefitSettings
{
AuthorizationHeaderValueGetter = () => accessTokenHelperService.GetAccessToken(default)
};
})
.ConfigureHttpClient((sp, client) =>
{
var BarSettings = sp.GetRequiredService<IOptions<BarApiSettings>>();
string baseUrl = BarSettings.Value.BaseUrl;
client.BaseAddress = new Uri(baseUrl);
})
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(x => x.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
.WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), RetryPolicyMaxCount)));
return services;
}
Are there any caveats with the current implementation? I am only interested in possible security, performance or "reinventing-the-wheel" issues.

Refresh token on asp.net web api and Blazor server side

I have an application where the backend is an asp.net web api and the front-end is a Blazor server side. Both projects are using net6.0.
I have implemented jwt token authentication, so users can register and login from the front-end.
My problem is that if the user refreshes a page, he automatically gets logged out. My understanding is that this can be solved using refresh token (I'm not sure if this understanding is correct).
I have tried to follow this guide: Refresh Token with Blazor WebAssembly and ASP.NET Core Web API
However since I'm using Blazor server side I cannot intercept HTTP Requests using the approach in the article.
My question is: in my Blazor server side application how can I prevent users automatically getting logged out due to page refresh and how can I intercept the http request?
UPDATE: Notice I already have everything working in regards to token and authentication between the back and frontend. The part that I'm missing is inside the blazor server side application in the program.cs file. I basically want to intercept all http request and call a method.
In program.cs I have:
builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
I want RefreshTokenService to be called on every http request. I have tried creating a middleware (which calls the RefreshTokenService), inside the program.cs like:
app.UseMyMiddleware();
But this only get called once.
Here's a very simplified version of an API client I'm using in my app that's also split into an ASP.NET Core API backend and a Blazor Server frontend.
The way it works is that the accessToken gets retreived from local storage and added as an authentication header to the HttpRequestMessage in my API client before each API call.
MyApiClient.cs
public class MyApiClient
{
private readonly IHttpClientFactory _clientFactory;
private readonly IMyApiTokenProvider _myApiTokenProvider;
public MyApiClient(IHttpClientFactory clientFactory, IMyApiTokenProvider myApiTokenProvider)
{
_clientFactory = clientFactory;
_myApiTokenProvider = myApiTokenProvider;
}
public async Task<ApiResponse<CustomerListResponse>> GetCustomersAsync()
{
//create HttpClient
var client = _clientFactory.CreateClient("MyApiHttpClient");
//create HttpRequest
var request = CreateRequest(HttpMethod.Get, "/getCustomers");
//call the API
var response = await client.SendAsync(request);
//if Unauthorized, refresh access token and retry
if(response.StatusCode == HttpStatusCode.Unauthorized)
{
var refreshResult = await RefreshAccessToken(client);
if (refreshResult.IsSuccess)
{
//save new token
await _backendTokenProvider.SetAccessToken(refreshResult.NewAccessToken);
//create request again, with new access token
var retryRequest = await CreateRequest(HttpMethod.Get, "/getCustomers");
//retry
response = await client.SendAsync(retryRequest);
}
else
{
//refresh token request failed
return ApiResponse<CustomerListResponse>.Error("Token invalid");
}
}
//parse response
var customers = await response.Content.ReadFromJsonAsync<ApiResponse<CustomerListResponse>>();
return customers;
}
private HttpRequestMessage CreateRequest<TRequest>(string command, HttpMethod method, TRequest requestModel = null) where TRequest : class
{
//create HttpRequest
var request = new HttpRequestMessage(method, command);
//add body if not empty
if (requestModel is not null)
{
request.Content = JsonContent.Create(requestModel);
}
//set the Auth header to the Access Token value taken from Local Storage
var accessToken = await _myApiTokenProvider.GetAccessToken();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return request;
}
private async Task<ApiResponse<RefreshTokenResponse>> RefreshAccessToken(HttpClient client)
{
var refreshToken = await _backendTokenProvider.GetRefreshToken();
if (refreshToken is null)
{
return ApiResponse<RefreshTokenResponse>.Error("Refresh token is null, cannot refresh access token");
}
var refreshRequest = CreateRequest(HttpMethod.Post, "/refreshToken", new RefreshTokenRequest(refreshToken));
var refreshResponse = await client.SendAsync(refreshRequest);
var refreshResult = await response.Content.ReadFromJsonAsync<ApiResponse<RefreshTokenResponse>>();
return refreshResult;
}
}
MyApiTokenProvider.cs
public class MyApiTokenProvider : IMyApiTokenProvider
{
private readonly ProtectedLocalStorage _protectedLocalStorage;
public MyApiTokenProvider(ProtectedLocalStorage protectedLocalStorage)
{
_protectedLocalStorage = protectedLocalStorage;
}
public async Task<string> GetAccessToken()
{
var result = await _protectedLocalStorage.GetAsync<string>("accessToken");
return result.Success ? result.Value : null;
}
public async Task<string> GetRefreshToken()
{
var result = await _protectedLocalStorage.GetAsync<string>("refreshToken");
return result.Success ? result.Value : null;
}
public async Task SetAccessToken(string newAccessToken)
{
await _protectedLocalStorage.SetAsync("accessToken", newAccessToken);
}
public async Task SetRefreshToken(string newRefreshToken)
{
await _protectedLocalStorage.SetAsync("refreshToken", newRefreshToken);
}
}

Error in ASP.NET Core MVC and Web API project

I have an ASP.NET Core MVC and also Web API project.
This error occurs when I try to send project information to the API (of course API works fine and I do not think there is a problem):
UnsupportedMediaTypeException: No MediaTypeFormatter is available to read a "TokenModel" object of "text / plain" media content.
My code is:
public class TokenModel
{
public string Token { get; set; }
}
and in AuthController I have:
var _Client = _httpClientFactory.CreateClient("MyApiClient");
var jsonBody = JsonConvert.SerializeObject(login);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
var response = _Client.PostAsync("/Api/Authentication", content).Result;
if (response.IsSuccessStatusCode)
{
var token = response.Content.ReadAsAsync<TokenModel>().Result;
}
The error occurs on this line:
var token = response.Content.ReadAsAsync<TokenModel>().Result;
HomeController:
public IActionResult Index()
{
var token = User.FindFirst("AccessToken").Value;
return View(_user.GetAllUsers(token));
}
UserRepository:
public List<UserViewModel> GetAllUsers(string token)
{
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var res = _client.GetStringAsync(UrlMyApi).Result;
List<UserViewModel> users = JsonConvert.DeserializeObject<List<UserViewModel>>(res);
return users;
}
Your API is returning content-type of text/plain and none of the default media type formatters(MediaTypeFormatter) which ReadAsAsync<string>() will try to use support parsing it as is. They work with JSON/XML. You can go a couple of ways but maybe the easiest is to read the content as string and deserialize it after:
var tokenJSON = response.Content.ReadAsStringAsync().Result;
var token = JsonConvert.DeserializeObject<TokenModel>(tokenJSON);
Also, as you're using the Async methods, you should be returning Task from your actions and await the result instead of using .Result as you're just creating overhead currently.
var tokenJSON = await response.Content.ReadAsStringAsync();
var token = JsonConvert.DeserializeObject<TokenModel>(tokenJSON);

HttpClient not sending post data to NancyFX endpoint

I am doing some integration testing of my web API that uses NancyFX end points. I have the xUnit test create a test server for the integration test
private readonly TestServer _server;
private readonly HttpClient _client;
public EventsModule_Int_Tester()
{
//Server setup
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_server.AllowSynchronousIO = true;//Needs to be overriden in net core 3.1
_client = _server.CreateClient();
}
Inside a Test Method I tried the following
[Fact]
public async Task EventTest()
{
// Arrange
HttpResponseMessage expectedRespone = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
var data = _server.Services.GetService(typeof(GenijalnoContext)) as GenijalnoContext;
//Get come random data from the DBcontext
Random r = new Random();
List<Resident> residents = data.Residents.ToList();
Resident random_residnet = residents[r.Next(residents.Count)];
List<Apartment> apartments = data.Apartments.ToList();
Apartment random_Apartment = apartments[r.Next(apartments.Count)];
EventModel model = new EventModel()
{
ResidentId = random_residnet.Id,
ApartmentNumber = random_Apartment.Id
};
//Doesnt work
IList<KeyValuePair<string, string>> nameValueCollection = new List<KeyValuePair<string, string>> {
{ new KeyValuePair<string, string>("ResidentId", model.ResidentId.ToString()) },
{ new KeyValuePair<string, string>("ApartmentNumber", model.ApartmentNumber.ToString())}
};
var result = await _client.PostAsync("/Events/ResidentEnter", new FormUrlEncodedContent(nameValueCollection));
//Also Doesnt work
string json = JsonConvert.SerializeObject(model, Formatting.Indented);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/Events/ResidentEnter", httpContent);
//PostAsJsonAsync also doesnt work
// Assert
Assert.Equal(response.StatusCode, expectedRespone.StatusCode);
}
The NancyFX module does trigger the endpoint and receives the request but without the body
What am I doing wrong? Note that the NancyFX endpoint has no issue transforming a Postman call into a valid model.
The NancyFX endpoint
Alright I fixed it, for those curious the issue was that the NancyFX body reader sometimes does not properly start reading the request body. That is that the stream reading position isn't 0 (the start) all the time.
To fix this you need to create a CustomBoostrapper and then override the ApplicationStartup function so you can set up a before request pipeline that sets the body position at 0
Code below
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
pipelines.BeforeRequest.AddItemToStartOfPipeline(ctx =>
{
ctx.Request.Body.Position = 0;
return null;
});
}

Asp.Net Core - Making API calls from backend

I have an application which is calling API's from a backend cs class, using IHostedService. With basic API calls ("http://httpbin.org/ip") it is working fine and returning the correct value, however I now need to call a Siemens API which requires me to set an Authorization header, and place "grant_type=client_credentials" in the body.
public async Task<string> GetResult()
{
string data = "";
string baseUrl = "https://<space-name>.mindsphere.io/oauth/token";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", {ServiceCredentialID: ServiceCredentialSecret});
using (HttpResponseMessage res = await client.GetAsync(baseUrl))
{
using (HttpContent content = res.Content)
{
data = await content.ReadAsStringAsync();
}
}
}
I think I have the header set up correctly but I won't know for sure until the full request gets formatted. Is it even possible to set the the body of the request to "grant_type=client_credentials"?
As far as I can see from Siemens API documentation they expect Form data, so it should be like:
public async Task<string> GetResult()
{
string data = "";
string baseUrl = "https://<space-name>.mindsphere.io/oauth/token";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", {ServiceCredentialID: ServiceCredentialSecret});
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials")
});
using (HttpResponseMessage res = await client.PostAsync(baseUrl, formContent))
{
using (HttpContent content = res.Content)
{
data = await content.ReadAsStringAsync();
}
}
}
}