Cannot write to a HttpContext.Response in AuthenticationHandler - c#

When in my custom AuthenticationHandler I want to write some data into HttpContext.Response like this:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
await Context.Response.WriteAsJsonAsync(new SomeObject(), Context.RequestAborted);
return AuthenticateResult.Fail("something went wrong");
}
I always get exception:
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: StatusCode cannot be set because the response has already started.
With these stack traces:
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler1.HandleChallengeAsync(AuthenticationProperties properties) at Microsoft.AspNetCore.Authentication.AuthenticationHandler1.ChallengeAsync(AuthenticationProperties properties)
at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.<>c__DisplayClass0_0.<g__Handle|0>d.MoveNext()
--- End of stack trace from previous location ---
And:
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
Thought with this error my request is aborted and I don't get any response. If after writing to response I call Response.CompleteAsync() then exception is still thrown, but I get response.
Additionally, I don't have any custom middleware. How can I fix this?

Related

ASP.NET Core Web API - JWT Message Handler - No Registered Type Error

I have created a JWT Token Handler for my Web API project. The idea is that this API is merely a gateway to other APIs and a token is shared across them all. So when a call is made to my API and I am forwarding the call to another, I want to automatically forward the JWT that was attached to the initial request.
I made a custom MessageHandler as seen below:
public class JwtTokenHeaderHandler : DelegatingHandler
{
private readonly HttpContext httpContext;
public JwtTokenHeaderHandler(HttpContext httpContext)
{
this.httpContext = httpContext;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
string savedToken = await this.httpContext.GetTokenAsync("access_token");
request.Headers.Add("bearer", savedToken);
return await base.SendAsync(request, cancellationToken);
}
}
Then I registered it as a service in the Startup as below:
services.AddTransient<HttpMessageHandler>(p => p.GetRequiredService<JwtTokenHeaderHandler>());
IHttpClientBuilder serviceClientBuilder = services.AddHttpClient<IService, Service>(client =>
{
client.BaseAddress = new Uri(this.Configuration.GetValue<string>("ServiceURL"));
}).AddHttpMessageHandler<JwtTokenHeaderHandler>();
The application runs up until a request is made and then this exception comes up:
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: No service for type 'Application.API1.JwtTokenHeaderHandler' has been registered.
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.<>c__4`1.b__4_1(HttpMessageHandlerBuilder b)
at Microsoft.Extensions.Http.DefaultHttpClientFactory.<>c__DisplayClass17_0.g__Configure|0(HttpMessageHandlerBuilder b)
at Microsoft.Extensions.Http.LoggingHttpMessageHandlerBuilderFilter.<>c__DisplayClass3_0.b__0(HttpMessageHandlerBuilder builder)
First, injecting HttpContext directly will cause issues. Use IHttpContextAccessor and access the context in the Send method.
public class JwtTokenHeaderHandler : DelegatingHandler {
private readonly IHttpContextAccessor accessor;
public JwtTokenHeaderHandler(IHttpContextAccessor accessor) {
this.accessor = accessor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
string savedToken = await accessor.HttpContext.GetTokenAsync("access_token");
request.Headers.Add("bearer", savedToken);
return await base.SendAsync(request, cancellationToken);
}
}
Next GetRequiredService throws that exception when the requested service is not registered with the collection so make sure to register the handler with the container
Note
The handler type must be registered as a transient service.
Reference AddHttpMessageHandler<THandler>(IHttpClientBuilder)
services.AddTransient<JwtTokenHeaderHandler>();
So that JwtTokenHeaderHandler can be resolved when requested from the service provider
services.AddTransient<HttpMessageHandler>(p => p.GetRequiredService<JwtTokenHeaderHandler>());
services.AddHttpClient<IService, Service>(client => {
client.BaseAddress = new Uri(this.Configuration.GetValue<string>("ServiceURL"));
}).AddHttpMessageHandler<JwtTokenHeaderHandler>();

Passing collection in querystring does not work with ODataQueryOptions

I have following existing GET endpoint
[HttpGet]
public async Task<IHttpActionResult> Get(ODataQueryOptions<Section> oDataOptions) {
//code here
return response;
}
I need to add a collection in querystring like <basequery>?$filter=id eq 1&includeCategory=A&includeCategory=B
so this would be bound as a collection in modified get endpoint as follows -
[HttpGet]
public async Task<IHttpActionResult> Get([FromUri (Name = "includeCategory"] ) List<Category> categories, ODataQueryOptions<Section> oDataOptions) {
//code here
return response;
}
Public enum Category {
A,
B
}
If I make postman request to this, I am getting An item with the same key has already been added.
System.ArgumentException: An item with the same key has already been added.
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
at System.Web.OData.Query.ODataQueryOptions..ctor(ODataQueryContext context, HttpRequestMessage request)
at System.Web.OData.Query.ODataQueryOptions`1..ctor(ODataQueryContext context, HttpRequestMessage request)
at System.Web.OData.ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.CreateODataQueryOptions[T](ODataQueryContext context, HttpRequestMessage request)
at System.Web.OData.ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
at System.Web.Http.Controllers.HttpActionBinding.<ExecuteBindingAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()
If my querystring has this <basequery>?$filter=id eq 1&includeCategory=A, this is working fine with ODataQueryOptions.
If my querystring has this <basequery>?$filter=id eq 1&includeCategory[0]=A&includeCategory[0]=B, this is working fine as well with ODataQueryOptions .
Same works for following with url <basequery>/1?includeCategory=A&includeCategory=B and endpoint with no $filter
[Route("{id}")
[HttpGet]
public async Task<IHttpActionResult> Get([FromUri (Name = "includeCategory"] ) List<Category> categories, long id) {
//code here
return response;
}
There is nothing in Section class that might collide with name Category.
That's why I am suspecting it's something to do with $filer, please suggest.

How to customize error code in custom AuthenticationHandler

There is a customized AuthenticationHandler named CustomAuthenticationHandler which default error code is 401. But I have to response with different error code and error message in different conditions.
If the request should response 403 in some condition and current solution shows below:
public class CustomAuthenticationHandler: AuthenticationHandler<MSGraphAuthenticationOptions>
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (xxx)
{
var response = this.httpContextAccessor.HttpContext.Response;
response.StatusCode = StatusCodes.Status403Forbidden;
await response.WriteAsync("test");
return AuthenticateResult.NoResult();
}
}
}
The error code of the http response is 403 which is expected, but the reqeust still run into next() and it would throw error:
System.InvalidOperationException: StatusCode cannot be set because the response has already started.
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.ThrowResponseAlreadyStartedException(String name)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.HandleChallengeAsync(AuthenticationProperties properties)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.ChallengeAsync(AuthenticationProperties properties)
at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.Management.Services.CloudPC.Api.Middlewares.MetricsMiddleware.InvokeAsync(HttpContext context, ILoggerX logger)
How can I stop the middlerware flow after await response.WriteAsync("test");?
If your authentication fails, you should call AuthenticateResult.Fail("<your custom message here>"); so the rest of the pipeline doesn't get executed.
Anyway, 403 error message is returned when Authorization fails, not in the case of Authentication fail, so you might set your authorization policies as described here: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/simple?view=aspnetcore-3.1

ASP.NET Core Integration Test works locally but throws null reference exception when running in production environment

I have an ASP.NET Core 2.2 Razor Pages Web App that I have written some integration tests for following the official guide.
I can get the tests to run locally using dotnet test or the test runners built into Visual Studio. However, on the build server (Azure DevOps Hosted 2017 agent) the tests will return a 500 error. I thought it might be related to user secrets as stated on Scott Hanselman's guide but I am still getting the same error, even after implementing some of his suggested fixes (I don't believe I need all of them) :
Added builder.AddUserSecrets<Startup>(); to Startup.
Implemented a CustomWebApplicationFactory to set the environment to "Development" - code below has this as "Production" to reproduce the failure.
I also sanity checked against this guide which is more controller focused but since I only care about the response codes at this stage it serves my purpose. I have downloaded the verbose logs and they don't shed any light on the issue.
My code is below:
CustomWebApplicationFactory:
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
namespace WebPortal.Int.Tests
{
/// <summary>
/// Based on https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api
/// </summary>
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup> where TStartup : class
{
public CustomWebApplicationFactory() { }
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.ConfigureTestServices(
services =>
{
services.Configure(AzureADDefaults.OpenIdScheme, (System.Action<OpenIdConnectOptions>)(o =>
{
// CookieContainer doesn't allow cookies from other paths
o.CorrelationCookie.Path = "/";
o.NonceCookie.Path = "/";
}));
}
)
.UseEnvironment("Production")
.UseStartup<Startup>();
}
}
}
AuthenticationTests:
using Microsoft.AspNetCore.Mvc.Testing;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace WebPortal.Int.Tests
{
public class AuthenticationTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private HttpClient _httpClient { get; }
public AuthenticationTests(CustomWebApplicationFactory<Startup> fixture)
{
WebApplicationFactoryClientOptions webAppFactoryClientOptions = new WebApplicationFactoryClientOptions
{
// Disallow redirect so that we can check the following: Status code is redirect and redirect url is login url
// As per https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2#test-a-secure-endpoint
AllowAutoRedirect = false
};
_httpClient = fixture.CreateClient(webAppFactoryClientOptions);
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/Error")]
public async Task Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(string url)
{
// Act
HttpResponseMessage response = await _httpClient.GetAsync(url);
// Assert
try
{
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
Console.WriteLine(ex.Message, ex.InnerException.Message);
}
}
}
}
Startup:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebPortal.Authentication;
using WebPortal.Common.ConfigurationOptions;
using WebPortal.DataAccess;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
namespace WebPortal
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddOptions<PowerBiSettings>()
.Bind(Configuration.GetSection("PowerBI"))
.ValidateDataAnnotations()
.Validate(o => o.AreSettingsValid());
services.AddOptions<AzureActiveDirectorySettings>()
.Bind(Configuration.GetSection("AzureAd"))
.ValidateDataAnnotations()
.Validate(o => o.AreSettingsValid());
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options))
.AddCookie();
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
});
services.AddTransient<Authentication.IAuthenticationHandler, AuthenticationHandler>();
services.AddTransient<IReportRepository, ReportRepository>();
services.AddHttpContextAccessor();
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Reports");
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var builder = new ConfigurationBuilder();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
builder.AddUserSecrets<Startup>();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc();
}
}
}
Error output:
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.1 (64-bit .NET Core 4.6.27317.07)
[xUnit.net 00:00:01.30] Discovering: WebPortal.Int.Tests
[xUnit.net 00:00:01.40] Discovered: WebPortal.Int.Tests
[xUnit.net 00:00:01.41] Starting: WebPortal.Int.Tests
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\VssAdministrator\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[58]
Creating key {dd820f09-8139-4d7d-954a-399923660f42} with creation date 2019-03-18 22:13:27Z, activation date 2019-03-18 22:13:27Z, and expiration date 2019-06-16 22:13:27Z.
info: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[39]
Writing data to file 'C:\Users\VssAdministrator\AppData\Local\ASP.NET\DataProtection-Keys\key-dd820f09-8139-4d7d-954a-399923660f42.xml'.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/2.0 GET http://localhost/Index
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
Failed to determine the https port for redirect.
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ArgumentNullException: Value cannot be null.
Parameter name: uriString
at System.Uri..ctor(String uriString)
at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
at System.Lazy`1.CreateValue()
at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[3]
An exception was thrown attempting to execute the error handler.
System.ArgumentNullException: Value cannot be null.
Parameter name: uriString
at System.Uri..ctor(String uriString)
at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
--- End of stack trace from previous location where exception was thrown ---
at System.Lazy`1.CreateValue()
at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 449.9633ms 500
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/2.0 GET http://localhost/Error
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ArgumentNullException: Value cannot be null.
Parameter name: uriString
at System.Uri..ctor(String uriString)
at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
--- End of stack trace from previous location where exception was thrown ---
at System.Lazy`1.CreateValue()
at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61] WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(url: "/Index") [FAIL]
[xUnit.net 00:00:02.61] System.ArgumentNullException : Value cannot be null.
[xUnit.net 00:00:02.61] Parameter name: uriString
[xUnit.net 00:00:02.61] Stack Trace:
[xUnit.net 00:00:02.61] at System.Uri..ctor(String uriString)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
[xUnit.net 00:00:02.61] at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
[xUnit.net 00:00:02.61] at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
[xUnit.net 00:00:02.61] at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
[xUnit.net 00:00:02.61] --- End of stack trace from previous location where exception was thrown ---
[xUnit.net 00:00:02.61] at System.Lazy`1.CreateValue()
[xUnit.net 00:00:02.61] at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
[xUnit.net 00:00:02.61] at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.TestHost.HttpContextBuilder.<>c__DisplayClass10_0.<<SendAsync>b__0>d.MoveNext()
[xUnit.net 00:00:02.61] --- End of stack trace from previous location where exception was thrown ---
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.TestHost.ClientHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Mvc.Testing.Handlers.CookieContainerHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
[xUnit.net 00:00:02.61] at Microsoft.AspNetCore.Mvc.Testing.Handlers.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)[xUnit.net 00:00:02.63] WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(url: "/Error") [FAIL]
[xUnit.net 00:00:02.64] WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(url: "") [FAIL]
[xUnit.net 00:00:02.61] at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
[xUnit.net 00:00:02.61] D:\a\1\s\WebPortal.Int.Tests\AuthenticationTests.cs(24,0): at WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(String url)
[xUnit.net 00:00:02.61] --- End of stack trace from previous location where exception was thrown ---
edit
It isn't very clear to me where/why I am getting a null reference because as far as I can tell my OpenIdConnectOptions configuration is correct (and it works with AAD SSO).
It turns out that this was because I hadn't called .Build() on my ConfigurationBuilder object that I had created, AND assigned the value to the Configuration field inside the Startup class. This also meant that I moved the code to include the secrets into the Startup constructor.
Even so, my secrets were still not accessible on the build machine (makes sense because they are stored per machine). So I had to add a Command Line task to my build pipeline as well - which uses the dotnet user-secrets set command to add the secrets in that are required for the tests.
Is it possible that you were missing the AzureAd configuration section from you production settings file? i.e. appsettings.json vs appsettings.Development.json

CreateDocumentQuery throws exception when used with LINQ, fine with SQL

I am attempting to retrieve a single entity stored in my Azure DocumentDb. I have discovered that my code only works when I supply the query by SQL, like the below:
var query = String.Format("SELECT * FROM UserDetail u WHERE u.id = '{0}'", id);
return _client.CreateDocumentQuery<TEntity>(_selfLink, query).AsEnumerable().FirstOrDefault();
However using a LINQ expression, as in the following two examples, it fails with an exception:
// EXCEPTION
return _client.CreateDocumentQuery<TEntity>(_selfLink).Where(u => u.Id.ToString() == id).AsEnumerable().FirstOrDefault();
// EXCEPTION
return (from u in _client.CreateDocumentQuery<TEntity>(_selfLink)
where u.Id.ToString() == id
select u).AsEnumerable().FirstOrDefault();
The rather monstrous stack trace of the exception raised is:
System.AggregateException: One or more errors occurred. ---> Microsoft.Azure.Documents.Linq.DocumentQueryException: Unhandled expression type: 'Call'
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitScalarExpression(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitBinary(BinaryExpression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitScalarExpression(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitScalarLambda(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitWhere(ReadOnlyCollection`1 arguments, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitMethodCall(MethodCallExpression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.Translate(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.TranslateQuery(Expression inputExpression)
at Microsoft.Azure.Documents.Linq.SQLTranslator.TranslateQuery(Expression inputExpression)
at Microsoft.Azure.Documents.Linq.DocumentQueryEvaluator.HandleMethodCallExpression(MethodCallExpression expression, QueryType defaultQueryType, QueryType& queryType)
at Microsoft.Azure.Documents.Linq.DocumentQueryEvaluator.Evaluate(Expression expression, QueryType defaultQueryType, QueryType& queryType)
at Microsoft.Azure.Documents.Linq.DocumentQueryExecutionContext.<ExecuteAllAsync>d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Microsoft.Azure.Documents.Linq.DocumentQuery`1.<GetEnumeratorTAsync>d__10.MoveNext()
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
at System.Threading.Tasks.Task`1.get_Result()
at Microsoft.Azure.Documents.Linq.DocumentQuery`1.GetEnumerator()
at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)
at Cherish.Domain.Repositories.Implementation.DocumentRepository`1.FindById(String id) in \\psf\home\Documents\Visual Studio 2013\Projects\Cherish\Cherish.Domain\Repositories\Implementation\DocumentRepository.cs:line 82
---> (Inner Exception #0) Microsoft.Azure.Documents.Linq.DocumentQueryException: Unhandled expression type: 'Call'
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitScalarExpression(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitBinary(BinaryExpression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitScalarExpression(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitScalarLambda(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitWhere(ReadOnlyCollection`1 arguments, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.VisitMethodCall(MethodCallExpression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.Translate(Expression inputExpression, TranslationContext context)
at Microsoft.Azure.Documents.Linq.ExpressionToSql.TranslateQuery(Expression inputExpression)
at Microsoft.Azure.Documents.Linq.SQLTranslator.TranslateQuery(Expression inputExpression)
at Microsoft.Azure.Documents.Linq.DocumentQueryEvaluator.HandleMethodCallExpression(MethodCallExpression expression, QueryType defaultQueryType, QueryType& queryType)
at Microsoft.Azure.Documents.Linq.DocumentQueryEvaluator.Evaluate(Expression expression, QueryType defaultQueryType, QueryType& queryType)
at Microsoft.Azure.Documents.Linq.DocumentQueryExecutionContext.<ExecuteAllAsync>d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Microsoft.Azure.Documents.Linq.DocumentQuery`1.<GetEnumeratorTAsync>d__10.MoveNext()<---
For reference, this is the UserDetail class (irrelevant properties and methods removed):
public class UserDetail : IIdentifiableEntity
{
[JsonProperty(PropertyName = "id")]
public Guid Id { get; set; }
[JsonProperty(PropertyName = "fn")]
public string Firstname { get; set; }
[JsonProperty(PropertyName = "ln")]
public string Lastname { get; set; }
[JsonProperty(PropertyName = "contribs")]
public IList<ContributorRelation> Contributors { get; set; }
}
I know that I can use the SQL-format method, however as a personal preference I would like to use Lambdas, and am at a loss to understand why my code does not work as it should. Do I need to do some other type of type coersion from Guid?
The call to the ToString method is not supported inside linq queries. Try parsing the id to guid and do a simple equality like this:
return _client.CreateDocumentQuery<TEntity>(_selfLink).Where(u => u.Id == Guid.Parse(id)).AsEnumerable().FirstOrDefault();

Categories

Resources