How to use MS Graph in a Action Filter - asp.net-core

I am trying to run an action filter any time one of my controllers is called from my front end to check if a token issued by Azure AD is still active. If I run this code in one of my controllers methods directly the code is fine and does as follows:
If token is expired or cache has been cleared, MS Graph SDK returns a redirect to MS Login
If token is valid it runs the MS Graph API and returns the results as normal.
In this ActionFilter below if the scenario is 1 the code just stops and errors out. In visual studio it actually generates the following service error
{Code: generalException
Message: An error occurred sending the request.
}
{Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
---> MSAL.NetCore.4.46.0.0.MsalUiRequiredException:
ErrorCode: user_null
Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.ApiConfig.Executors.ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable`1 scopes, String tenantId, MergedOptions mergedOptions, String userFlow, TokenAcquisitionOptions tokenAcquisitionOptions)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
StatusCode: 0
ResponseBody:
Headers:
--- End of inner exception stack trace ---
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
at Microsoft.Identity.Web.TokenAcquisitionAuthenticationProvider.AuthenticateRequestAsync(HttpRequestMessage request)
at Microsoft.Graph.AuthenticationHandler.SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at Microsoft.Graph.HttpProvider.SendRequestAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)}
So basically the ActionFilter does not automatically redirect the user to the MS Login screen or generate any response to send back to the front end for that matter. It just errors out even with a try and catch statement. So is there any way to do this? My action filter is set as follows:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Extensions.DependencyInjection;
using System;
public class CheckTokenFilter : IActionFilter
{
private readonly GraphServiceClient _graphServiceClient;
private readonly ITokenAcquisition _tokenAcquisition;
private readonly string[] initialScopes;
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler;
public CheckTokenFilter(GraphServiceClient graphServiceClient, ITokenAcquisition tokenAcquisition, IConfiguration configuration, MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
{
_graphServiceClient = graphServiceClient;
_tokenAcquisition = tokenAcquisition;
initialScopes = configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
this._consentHandler = consentHandler;
}
public void OnActionExecuted(ActionExecutedContext context)
{
//noop
}
public async void OnActionExecuting(ActionExecutingContext context)
{
User currentUser = null;
try
{
currentUser = await _graphServiceClient.Me.Request().GetAsync();
}
// Catch CAE exception from Graph SDK
catch (ServiceException svcex) when (svcex.Message.Contains("Continuous access evaluation resulted in claims challenge"))
{
try
{
Console.WriteLine($"{svcex}");
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
_consentHandler.ChallengeUser(initialScopes, claimChallenge);
}
catch (Exception ex2)
{
_consentHandler.HandleException(ex2);
}
}
}
}
Current method that is working based on answer below:
public class CheckTokenFilter : IActionFilter
{
private readonly GraphServiceClient _graphServiceClient;
private string SignedInRedirectUri;
public CheckTokenFilter(GraphServiceClient graphServiceClient, IConfiguration configuration)
{
_graphServiceClient = graphServiceClient;
SignedInRedirectUri = configuration["AzureAd:SignedInRedirectUri"];
}
public void OnActionExecuted(ActionExecutedContext context)
{
//noop
}
public async void OnActionExecuting(ActionExecutingContext context)
{
//First we check if token is valid and if not it will generate an error and challenge user causing a redirect which is caught in our program.cs. It sends a 403 error to the front end to load the login page.
User currentUser = null;
try
{
currentUser = await _graphServiceClient.Me.Request().GetAsync();
}
// Catch CAE exception from Graph SDK
catch
{
context.Result = new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme,
new AuthenticationProperties
{
IsPersistent = true,
RedirectUri = SignedInRedirectUri
});
}
}
}

What about using something like:
context.Result = new ChallengeResult(Your Scheme)
I use something similar in my solution, but I use:
[Authorize]
[ServiceFilter(typeof(*******AuthenticationFilter))]
This way if the token is not valid, it will go off and challenge the user before it goes to the filter. The filter can then do additional work once the user is authenticated at a high level. The filter just throws an exception in my case, as it shouldn't get into a bad state.

Related

Exception getting Azure AD groups via MS Graph with ASP.NET Core

I want to retrieve all groups assigned to a logged in user when user signed in. So that the groups can be used for the user's logged-in session.
I followed the code from the link below,
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/5-WebApp-AuthZ/5-2-Groups
and modified to call GetMyMemberOfGroupsAsync() like below, but get exception shown at the bottom.
public void ConfigureServices(IServiceCollection services)
{
...
// Sign-in users with the Microsoft identity platform
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(
options =>
{
Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents();
options.Events.OnTokenValidated = async context =>
{
var graphService = sp.GetService<IGraphService>();
await graphService.GetMyMemberOfGroupsAsync();
};
}, options => { Configuration.Bind("AzureAd", options); })
.EnableTokenAcquisitionToCallDownstreamApi(options => Configuration.Bind("AzureAd", options), initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("GraphAPI"))
.AddInMemoryTokenCaches();
...
}
Below is the code added to the code example to get all groups, but exception is thrown at the line of _graphServiceClient.Me.MemberOf.Request().GetAsync();
public class GraphService : IGraphService
{
private readonly GraphServiceClient _graphServiceClient;
public GraphService(GraphServiceClient graphServiceClient)
{
_graphServiceClient = graphServiceClient;
}
public async Task<List<string>> GetMyMemberOfGroupsAsync()
{
List<string> groups = new List<string>();
// Get groups the current user is a direct member of.
IUserMemberOfCollectionWithReferencesPage memberOfGroups = await _graphServiceClient.Me.MemberOf.Request().GetAsync(); //exception thrown
if (memberOfGroups?.Count > 0)
{
foreach (var directoryObject in memberOfGroups)
{
// We only want groups, so ignore DirectoryRole objects.
if (directoryObject is Group)
{
Group group = directoryObject as Group;
groups.Add(group.DisplayName);
}
}
}
// If paginating
while (memberOfGroups.NextPageRequest != null)
{
memberOfGroups = await memberOfGroups.NextPageRequest.GetAsync();
if (memberOfGroups?.Count > 0)
{
foreach (var directoryObject in memberOfGroups)
{
// We only want groups, so ignore DirectoryRole objects.
if (directoryObject is Group)
{
Group group = directoryObject as Group;
groups.Add(group.DisplayName);
}
}
}
}
return groups;
}
}
Exception below is thrown at the line of _graphServiceClient.Me.MemberOf.Request().GetAsync();
System.Exception: An error was encountered while handling the remote login.
---> Status Code: 0
Microsoft.Graph.ServiceException: Code: generalException
Message: An error occurred sending the request.
---> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
---> MSAL.NetCore.4.18.0.0.MsalUiRequiredException:
ErrorCode: user_null
Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
at Microsoft.Identity.Client.AcquireTokenSilentParameterBuilder.Validate()
at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder`1.ValidateAndCalculateApiId()
at Microsoft.Identity.Client.AbstractClientAppBaseAcquireTokenParameterBuilder`1.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder`1.ExecuteAsync()
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, IAccount account, IEnumerable`1 scopes, String authority, String userFlow)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable`1 scopes, String authority, String userFlow)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user)
StatusCode: 0
ResponseBody:
Headers:
--- End of inner exception stack trace ---
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user)
at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user)
at Microsoft.Identity.Web.TokenAcquisitionCredentialProvider.AuthenticateRequestAsync(HttpRequestMessage request)
at Microsoft.Graph.AuthenticationHandler.SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
at Microsoft.Graph.HttpProvider.SendRequestAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.Graph.HttpProvider.SendRequestAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
at Microsoft.Graph.HttpProvider.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
at Microsoft.Graph.BaseRequest.SendRequestAsync(Object serializableObject, CancellationToken cancellationToken, HttpCompletionOption completionOption)
at Microsoft.Graph.BaseRequest.SendAsync[T](Object serializableObject, CancellationToken cancellationToken, HttpCompletionOption completionOption)
at Microsoft.Graph.UserMemberOfCollectionWithReferencesRequest.GetAsync(CancellationToken cancellationToken)
at WebApp_OpenIDConnect_DotNet.Controllers.GraphService.GetMyMemberOfGroupsAsync() in C:\_MyLab\AzureCode\ActiveDirectory\active-directory-aspnetcore-webapp-openidconnect-v2-v2\5-WebApp-AuthZ\5-2-Groups\Controllers\GraphService.cs:line 20
at WebApp_OpenIDConnect_DotNet.Startup.<>c__DisplayClass4_0.<<ConfigureServices>b__8>d.MoveNext() in C:\_MyLab\AzureCode\ActiveDirectory\active-directory-aspnetcore-webapp-openidconnect-v2-v2\5-WebApp-AuthZ\5-2-Groups\Startup.cs:line 62
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.<>c__DisplayClass10_1.<<WebAppCallsWebApiImplementation>b__2>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt, String nonce)
at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
If your app requests more scopes than what the admin has consented, you'll receive an MsalUiRequiredException, so make sure that you have granted sufficient permissions to your application and granted admin consent to that permission.
If you need to retrieve all the groups assigned to the logged-in user when the user logs in, please refer to the following code:
GraphServiceClient graphClient = new GraphServiceClient( authProvider );
var securityEnabledOnly = true;
await graphClient.Me
.GetMemberGroups(securityEnabledOnly)
.Request()
.PostAsync();

Asp.net core healthchecks randomly fails with TaskCanceledException or OperationCanceledException

I've implemented healthchecks in my asp.net core application.
One healthcheck does 2 checks - DbContext connection and custom one that checks NpgsqlConnection.
Everything works fine in over 99% of cases. Occasionally healthcheck fails throwing TaskCanceledException or OperationCanceledException. From my logs I can see that this exceptions are thrown after around 2ms-25ms (so there is no chance any timeout happened).
Important hint:
When I hit healtchecks many times (simple F5 in browser) it throws the exception. Looks like you can't hit /health endpoint before previous healthcheck is completed. If this is the case - why? Even if I put Thread.Sleep(5000); in custom healthcheck (an no DB connection check at all) it will fail if I hit /health endpoint before 5 seconds passes.
QUESTION: Is healtheck somehow 'magically' single-threaded (when you hit that endpoint again, it cancels previous healthcheck invocation)?
Startup.cs ConfigureServices
services
.AddHealthChecks()
.AddCheck<StorageHealthCheck>("ReadOnly Persistance")
.AddDbContextCheck<MyDbContext>("EFCore persistance");
Startup.cs Configure
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseMiddleware<RequestLogMiddleware>();
app.UseMiddleware<ErrorLoggingMiddleware>();
if (!env.IsProduction())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "V1");
c.SwaggerEndpoint($"/swagger/v2/swagger.json", $"V2");
});
}
app.UseHealthChecks("/health", new HealthCheckOptions()
{
ResponseWriter = WriteResponse
});
app.UseMvc();
StorageHealthCheck.cs
public class StorageHealthCheck : IHealthCheck
{
private readonly IMediator _mediator;
public StorageHealthCheck(IMediator mediator)
{
_mediator = mediator;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var isReadOnlyHealthy = await _mediator.Send(new CheckReadOnlyPersistanceHealthQuery());
return new HealthCheckResult(isReadOnlyHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy, null);
}
}
CheckReadOnlyPersistanceHealthQueryHandler:
NpgsqlConnectionStringBuilder csb = new NpgsqlConnectionStringBuilder(_connectionString.Value);
string sql = $#"
SELECT * FROM pg_database WHERE datname = '{csb.Database}'";
try
{
using (IDbConnection connection = new NpgsqlConnection(_connectionString.Value))
{
connection.Open();
var stateAfterOpening = connection.State;
if (stateAfterOpening != ConnectionState.Open)
{
return false;
}
connection.Close();
return true;
}
}
catch
{
return false;
}
TaskCanceledException:
System.Threading.Tasks.TaskCanceledException: A task was canceled.
at Npgsql.TaskExtensions.WithCancellation[T](Task`1 task, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnector.ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnector.RawOpen(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnection.<>c__DisplayClass32_0.<<Open>g__OpenLong|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlDatabaseCreator.ExistsAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Diagnostics.HealthChecks.DbContextHealthCheck`1.CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken)
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.CheckHealthAsync(Func`2 predicate, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware.InvokeAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware.Invoke(HttpContext context)
OperationCanceledException:
System.OperationCanceledException: The operation was canceled.
at System.Threading.CancellationToken.ThrowOperationCanceledException()
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.CheckHealthAsync(Func`2 predicate, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware.InvokeAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware.Invoke(HttpContext context)
I've finally found the answer.
The initial reason is that when the HTTP request is aborted, then httpContext.RequestAborted CancellationToken is triggered, and it throws an exception (OperationCanceledException).
I have a global exception handler in my application, and I have been converting every unhandled exception to a 500 error. Even though the client aborted the request, and never got the 500 response, my logs kept logging this.
The solution I implemented is like that:
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
if (context.RequestAborted.IsCancellationRequested)
{
_logger.LogWarning(ex, "RequestAborted. " + ex.Message);
return;
}
_logger.LogCritical(ex, ex.Message);
await HandleExceptionAsync(context, ex);
throw;
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
//if (ex is MyNotFoundException) code = HttpStatusCode.NotFound;
//else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
//else if (ex is MyException) code = HttpStatusCode.BadRequest;
var result = JsonConvert.SerializeObject(new { error = ex.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
hope it helps to somebody.
My best theory, after testing in a large production environment, is that you need to await any writers to the http context output stream in the health check. I was getting this error in a method where I returned a task that was not awaited. Awaiting the task appears to have solved the problem. The nice thing about await is that you could also catch a TaskCancelledException and just eat it.
Example:
// map health checks
endpoints.MapHealthChecks("/health-check", new HealthCheckOptions
{
ResponseWriter = HealthCheckExtensions.WriteJsonResponseAsync,
Predicate = check => check.Name == "default"
});
/// <summary>
/// Write a json health check response
/// </summary>
/// <param name="context">Http context</param>
/// <param name="report">Report</param>
/// <returns>Task</returns>
public static async Task WriteJsonResponseAsync(HttpContext context, HealthReport report)
{
try
{
HealthReportEntry entry = report.Entries.Values.FirstOrDefault();
context.Response.ContentType = "application/json; charset=utf-8";
await JsonSerializer.SerializeAsync(context.Response.Body, entry.Data,entry.Data.GetType());
}
catch (TaskCancelledException)
{
}
}

Adding Jwt HttpClient Handler with Blazor Asp.net Core 3.1.0 Preview 3

I recently upgraded my project from Asp.Net Core 3.0 to the latest build Asp.Net Core 3.1.0 preview 3.
My project is running Blazor in the client, and I also intercept the pipeline to add a Jwt to the header of every request sent back to the server using my JwtTokenHeaderHandler which extends DelegatingHandler.
I add this in the startup.cs of my blazor.client project as follows;
services.AddSingleton<HttpClient>(s =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var uriHelper = s.GetRequiredService<NavigationManager>();
var myHandler = s.GetRequiredService<JwtTokenHeaderHandler>();
myHandler.InnerHandler = new WebAssemblyHttpMessageHandler();
return new HttpClient(myHandler)
{
BaseAddress = new Uri(uriHelper.BaseUri)
};
});
However, since the upgrade it appears that WebAssemblyHttpMessageHandler() is no longer available?
I cannot find any documentation as to how to fix this approach.
Can anyone advise on how I can correct this please?
Update based on Aqua's answer
After adding the following code to my Startup.cs;
services.AddTransient(p =>
{
var wasmHttpMessageHandlerType = Assembly.Load("WebAssembly.Net.Http")
.GetType("WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler");
var constructor = wasmHttpMessageHandlerType.GetConstructor(Array.Empty<Type>());
return constructor.Invoke(Array.Empty<object>()) as HttpMessageHandler;
})
.AddTransient<JwtTokenHeaderHandler>();
Calling an Authorized Get endpoint from my .razor file;
var response = await Http.GetAsync<ContactTypeOutputModel[]>("ContactTypes/all");
(where the above is simply a typed wrapped service response using an extension method);
public static async Task<ServiceResponse<T>> GetAsync<T>(
this HttpClient httpClient, string url)
{
var response = await httpClient.GetAsync(url);
return await BuildResponse<T>(response);
}
I am still getting a 401 on the service? and no output logging to say the service is being used?
public class JwtTokenHeaderHandler : DelegatingHandler
{
private readonly IAppLocalStorageService _localStorage;
private readonly ILogger<JwtTokenHeaderHandler> _logger;
private readonly HttpMessageHandler _innerHandler;
private readonly MethodInfo _method;
public JwtTokenHeaderHandler(IAppLocalStorageService localStorage, HttpMessageHandler innerHandler, ILogger<JwtTokenHeaderHandler> logger)
{
_localStorage = localStorage;
_logger = logger;
_innerHandler = innerHandler;
var type = innerHandler.GetType();
_method = type.GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod) ?? throw new InvalidOperationException("Cannot get SendAsync method");
WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
_logger.LogDebug("Adding Jwt to Header from JwtTokenHeaderHandler: SendAsync");
_logger.LogDebug($"Does header contain 'bearer' token: {!request.Headers.Contains("bearer")}");
if (!request.Headers.Contains("bearer"))
{
_logger.LogDebug("Adding Bearer Token to Header");
var savedToken = await _localStorage.GetTokenAsync();
_logger.LogDebug($"Saved Token is : {savedToken}");
if (!string.IsNullOrWhiteSpace(savedToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", savedToken);
}
}
//return await base.SendAsync(request, cancellationToken);
return await (_method.Invoke(_innerHandler, new object[] { request, cancellationToken }) as Task<HttpResponseMessage>);
}
}
Any ideas why this is not being hit?
WebAssemblyHttpMessageHandler has been replaced by Mono's WasmHttpMessageHandler by you can't add a dependency to WebAssembly.Net.Http.
To create a DependencyHandler for blazor WASM, I did a little hack :
public class OidcDelegationHandler : DelegatingHandler
{
private readonly IUserStore _userStore;
private readonly HttpMessageHandler _innerHanler;
private readonly MethodInfo _method;
public OidcDelegationHandler(IUserStore userStore, HttpMessageHandler innerHanler)
{
_userStore = userStore ?? throw new ArgumentNullException(nameof(userStore));
_innerHanler = innerHanler ?? throw new ArgumentNullException(nameof(innerHanler));
var type = innerHanler.GetType();
_method = type.GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod) ?? throw new InvalidOperationException("Cannot get SendAsync method");
WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue(_userStore.AuthenticationScheme, _userStore.AccessToken);
return _method.Invoke(_innerHanler, new object[] { request, cancellationToken }) as Task<HttpResponseMessage>;
}
}
Is set it up in DI with:
return services
.AddTransient(p =>
{
var wasmHttpMessageHandlerType = Assembly.Load("WebAssembly.Net.Http")
.GetType("WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler");
var constructor = wasmHttpMessageHandlerType.GetConstructor(Array.Empty<Type>());
return constructor.Invoke(Array.Empty<object>()) as HttpMessageHandler;
})
The full code is here

difference between IAuthenticationFilter and IAuthorizationFilter in asp.net webapi 2?

We can achieve Basic authentication feature using AuthorizationFilterAttribute which uses IAuthorizationFilter Interface itself as below,
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Authorization == null)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
else
{
// Gets header parameters
string authenticationString = actionContext.Request.Headers.Authorization.Parameter;
string originalString = Encoding.UTF8.GetString(Convert.FromBase64String(authenticationString));
// Gets username and password
string usrename = originalString.Split(':')[0];
string password = originalString.Split(':')[1];
// Validate username and password
if (!ApiSecurity.VaidateUser(usrename, password))
{
// returns unauthorized error
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
base.OnAuthorization(actionContext);
}
}
Then why should we use IAuthenticationFilter and Attribute for basic authentication as below,
public class BasicAuthenticator : Attribute, IAuthenticationFilter
{
public Task AuthenticateAsync(HttpAuthenticationContext context,
CancellationToken cancellationToken)
{
}
public Task ChallengeAsync(
HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken) {}
}
I am new to these concepts in asp.net webapi 2, Please mention when to use AuthorizationFilterAttribute and when to use IAuthenticationFilter and Attribute for Basic authentication. Good Answer would be greatly appreciated !

WebAPI 2.0 call monitoring

Using ASP.NET WebAPI 2.0 and have a conceptual issue.
Would like to keep a global record of any API that is called by any user/ client and it would be awesome if this was stored in the database.
What would be the best mechanism to accomplish this?
I' using a DelegatingHandler for a long time in several projects which is doing just fine.
public class ApiCallLogHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
var started = DateTime.UtcNow;
HttpResponseMessage response = null;
Exception baseException = null;
try {
response = await base.SendAsync(request, cancellationToken);
} catch(Exception exception) {
CommonLogger.Logger.LogError(exception);
baseException = exception;
}
try {
var callModel = await GetCallModelAsync(request, response);
if(baseException != null)
callModel.Exception = baseException
callModel.ExecutionTime = (DateTime.UtcNow - started).ToString();
await CommonLogger.Logger.LogApiCallAsync(callModel);
} catch (Exception exception) {
CommonLogger.Logger.LogError(exception);
}
return response;
}
private async Task<ApiCallModel> GetCallModelAsync(HttpRequestMessage request, HttpResponseMessage response) {
// parse request and response and create a model to store in database...
}
}
By this approach you are able to track all requests, exceptions during execution, and even full-response of each API call.
ApiCallModel is just a simple POCO class which you should fill it with your required data from request and response.
CommonLogger.Logger.* is your logging mechanism.
And, you have to register the handler with this snippet:
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
config.MessageHandlers.Add(new ApiCallLogHandler());
}
}