Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET Core Identity Works with Normal Startup but not WebHostFactory #24740

Closed
pdevito3 opened this issue Aug 10, 2020 · 5 comments
Closed

.NET Core Identity Works with Normal Startup but not WebHostFactory #24740

pdevito3 opened this issue Aug 10, 2020 · 5 comments
Labels
area-identity Includes: Identity and providers

Comments

@pdevito3
Copy link

Describe what isn't working as expected

So I'm setting up an API in .NET Core 3.1 that is working fine when I run it locally, but when I run my xUnit project, I get this System.InvalidOperationException : Scheme already exists: Identity.Application error.

Now I started googling for this and it seems like the main resolution is generally to remove AddDefaultIdentity to either stop a clash with IdentityHostingStartup or prevent IdentityHostintgStartup.cs from causing some overlap.

For me, I'm not using AddDefaultIdentity and I'm not seeing a IdentityHostintgStartup.cs get generated, so I'm not quite sure what the deal is here.

Steps to reproduce

Running my integration tests should pass without the exception. When I remove the identity config from my startup and run my tests without it, they pass and the app runs fine with just a normal startup, so I'm guessing it's the way the the webhostfactory is running things.

Here is my Identity Service Registration that gets added to Startup

    public static class ServiceExtensions
    {
        public static void AddIdentityInfrastructure(this IServiceCollection services, IConfiguration configuration)
        {
            if (configuration.GetValue<bool>("UseInMemoryDatabase"))
            {
                services.AddDbContext<IdentityDbContext>(options =>
                    options.UseInMemoryDatabase("IdentityDb"));
            }
            else
            {
                services.AddDbContext<IdentityDbContext>(options =>
                options.UseSqlServer(
                    configuration.GetConnectionString("IdentityConnection"),
                    b => b.MigrationsAssembly(typeof(IdentityDbContext).Assembly.FullName)));
            }
            services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>().AddDefaultTokenProviders();

            #region Services
            services.AddScoped<IAccountService, AccountService>();
            #endregion

            services.Configure<IdentityOptions>(options =>
            {
                options.User.RequireUniqueEmail = true;

                options.Password.RequiredLength = 12;
                options.Password.RequireDigit = true;
                options.Password.RequireLowercase = true;
                options.Password.RequireUppercase = false;
                options.Password.RequireNonAlphanumeric = false;
            });

            services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
                .AddJwtBearer(o =>
                {
                    o.RequireHttpsMetadata = false;
                    o.SaveToken = false;
                    o.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.Zero,
                        ValidIssuer = configuration["JwtSettings:Issuer"],
                        ValidAudience = configuration["JwtSettings:Audience"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JwtSettings:Key"]))
                    };
                    o.Events = new JwtBearerEvents()
                    {
                        OnAuthenticationFailed = c =>
                        {
                            c.NoResult();
                            c.Response.StatusCode = 500;
                            c.Response.ContentType = "text/plain";
                            return c.Response.WriteAsync(c.Exception.ToString());
                        },
                        OnChallenge = context =>
                        {
                            context.HandleResponse();
                            context.Response.StatusCode = 401;
                            context.Response.ContentType = "application/json";
                            var result = JsonConvert.SerializeObject(new Response<string>("You are not Authorized"));
                            return context.Response.WriteAsync(result);
                        },
                        OnForbidden = context =>
                        {
                            context.Response.StatusCode = 403;
                            context.Response.ContentType = "application/json";
                            var result = JsonConvert.SerializeObject(new Response<string>("You are not authorized to access this resource"));
                            return context.Response.WriteAsync(result);
                        },
                    };
                });
        }
    }

here's my startup

    public class Startup
    {
        public IConfiguration _config { get; }
        public Startup(IConfiguration configuration)
        {
            _config = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors(options =>
            {
                options.AddPolicy("MyCorsPolicy",
                    builder => builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader());
            });

            services.AddApplicationLayer();
            services.AddIdentityInfrastructure(_config);
            services.AddPersistenceInfrastructure(_config);
            services.AddSharedInfrastructure(_config);
            //services.AddSwaggerExtension();
            services.AddControllers()
                .AddNewtonsoftJson();
            services.AddApiVersioningExtension();
            services.AddHealthChecks();
        }

        // https://autofaccn.readthedocs.io/en/latest/integration/aspnetcore.html
        /*public void ConfigureContainer(ContainerBuilder builder)
        {
        }*/

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();

                #region Entity Context Region - Do Not Delete
                using (var context = app.ApplicationServices.GetService<ValueToReplaceDbContext>())
                {
                    context.Database.EnsureCreated();

                    ValueToReplaceSeeder.SeedSampleValueToReplaceData(app.ApplicationServices.GetService<ValueToReplaceDbContext>());
                }
                #endregion

                var userManager = app.ApplicationServices.GetService<UserManager<ApplicationUser>>();
                var roleManager = app.ApplicationServices.GetService<RoleManager<IdentityRole>>();
                RoleSeeder.SeedDemoRolesAsync(roleManager);
                SuperAdminSeeder.SeedDemoSuperAdminsAsync(userManager);
                BasicUserSeeder.SeedDemoBasicUser(userManager);
            }

            app.UseCors("MyCorsPolicy");

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();
            app.UseErrorHandlingMiddleware();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHealthChecks("/api/health");
                endpoints.MapControllers();
            });
        }
    }

And here is my factory for my integration tests

public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
    {
        // checkpoint for respawn to clear the database when spenning up each time
        private static Checkpoint checkpoint = new Checkpoint
        {
            
        };

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.UseEnvironment("Testing");

            builder.ConfigureServices(services =>
            {
                services.AddEntityFrameworkInMemoryDatabase();

                // Create a new service provider.
                var provider = services
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                // Add a database context (ApplicationDbContext) using an in-memory 
                // database for testing.
                services.AddDbContext<ValueToReplaceDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                    options.UseInternalServiceProvider(provider);
                });

                services.AddDbContext<IdentityDbContext>(options =>
                {
                    options.UseInMemoryDatabase("Identity");
                    options.UseInternalServiceProvider(provider);
                });

                // Build the service provider.
                var sp = services.BuildServiceProvider();

                // Create a scope to obtain a reference to the database
                // context (ApplicationDbContext).
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ValueToReplaceDbContext>();
/*                    var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();

                    var logger = scopedServices
                        .GetRequiredService<ILogger<ApiTestFixture>>();*/

                    // Ensure the database is created.
                    db.Database.EnsureCreated();

                    try
                    {
                        // Seed the database with test data.
                        //ValueToReplaceDbContextSeed.SeedAsync(db, loggerFactory).Wait();

                        // seed sample user data
                        /*var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
                        var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
                        AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();*/
                    }
                    catch (Exception ex)
                    {
                        //logger.LogError(ex, $"An error occurred seeding the " +
                        //    "database with test messages. Error: {ex.Message}");
                    }
                }
            });
        }

        public HttpClient GetAnonymousClient()
        {
            return CreateClient();
        }
    }

Got Exceptions? Include both the message and the stack trace

Message: 
    Autofac.Core.DependencyResolutionException : An exception was thrown while activating Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider.
    ---- Autofac.Core.DependencyResolutionException : An exception was thrown while invoking the constructor 'Void .ctor(Microsoft.Extensions.Options.IOptions`1[Microsoft.AspNetCore.Authentication.AuthenticationOptions])' on type 'AuthenticationSchemeProvider'.
    -------- System.InvalidOperationException : Scheme already exists: Identity.Application
  Stack Trace: 
    InstanceLookup.Activate(IEnumerable`1 parameters, Object& decoratorTarget)
    <>c__DisplayClass7_0.<Execute>b__0()
    LifetimeScope.GetOrCreateAndShare(Guid id, Func`1 creator)
    InstanceLookup.Execute()
    ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, ResolveRequest request)
    ResolveOperation.ResolveComponent(ResolveRequest request)
    ResolveOperation.Execute(ResolveRequest request)
    LifetimeScope.ResolveComponent(ResolveRequest request)
    ResolutionExtensions.TryResolveService(IComponentContext context, Service service, IEnumerable`1 parameters, Object& instance)
    ResolutionExtensions.ResolveOptionalService(IComponentContext context, Service service, IEnumerable`1 parameters)
    ResolutionExtensions.ResolveOptional(IComponentContext context, Type serviceType, IEnumerable`1 parameters)
    ResolutionExtensions.ResolveOptional(IComponentContext context, Type serviceType)
    AutofacServiceProvider.GetService(Type serviceType)
    ConstructorMatcher.CreateInstance(IServiceProvider provider)
    ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
    <>c__DisplayClass4_0.<UseMiddleware>b__0(RequestDelegate next)
    ApplicationBuilder.Build()
    GenericWebHostService.StartAsync(CancellationToken cancellationToken)
    Host.StartAsync(CancellationToken cancellationToken)
    HostingAbstractionsHostExtensions.Start(IHost host)
    WebApplicationFactory`1.CreateHost(IHostBuilder builder)
    WebApplicationFactory`1.EnsureServer()
    WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
    WebApplicationFactory`1.CreateDefaultClient(Uri baseAddress, DelegatingHandler[] handlers)
    WebApplicationFactory`1.CreateClient(WebApplicationFactoryClientOptions options)
    CreateValueToReplaceIntegrationTests.PostInvalidValueToReplaceDateField1ReturnsBadRequestCode() line 82
    --- End of stack trace from previous location where exception was thrown ---
    ----- Inner Stack Trace -----
    ConstructorParameterBinding.Instantiate()
    ReflectionActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters)
    InstanceLookup.Activate(IEnumerable`1 parameters, Object& decoratorTarget)
    ----- Inner Stack Trace -----
    AuthenticationOptions.AddScheme(String name, Action`1 configureBuilder)
    <>c__DisplayClass4_0`2.<AddSchemeHelper>b__0(AuthenticationOptions o)
    ConfigureNamedOptions`1.Configure(String name, TOptions options)
    OptionsFactory`1.Create(String name)
    <>c__DisplayClass5_0.<Get>b__0()
    Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
    Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
    Lazy`1.CreateValue()
    Lazy`1.get_Value()
    OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
    OptionsManager`1.Get(String name)
    OptionsManager`1.get_Value()
    AuthenticationSchemeProvider.ctor(IOptions`1 options, IDictionary`2 schemes)
    lambda_method(Closure , Object[] )
    ConstructorParameterBinding.Instantiate()

Further technical details

EF Core version:
Database provider: Microsoft.EntityFrameworkCore.UseInMemoryDatabase
Target framework: .NET Core 3.1
Operating system:
IDE: Visual Studio 2019 16.6.5

@javiercn javiercn added the area-identity Includes: Identity and providers label Aug 10, 2020
@HaoK
Copy link
Member

HaoK commented Aug 12, 2020

Its not clear exactly how you are setting up your unit test, but clearly something is calling AddAuthentication with the same identity scheme twice. Maybe try commenting out the call to AddIdentity to confirm that makes the error go away, if another scheme throws that exception then that method is getting called more than once

@pdevito3
Copy link
Author

Thanks @HaoK, it does work after commenting out the addauth lines. Agreed about it getting added twice, but I'm not sure how other than possibly the identityhoststartup.cs that gets created sometimes (though i don't see it in my project).

As for the text context, here it a get endpoint test:

    [Collection("Sequential")]
    public class GetValueToReplaceIntegrationTests : IClassFixture<CustomWebApplicationFactory>
    {
        public GetValueToReplaceIntegrationTests(CustomWebApplicationFactory factory)
        {
            _factory = factory;
        }

        private readonly CustomWebApplicationFactory _factory;
        [Fact]
        public async Task GetValueToReplaces_ReturnsSuccessCodeAndResourceWithAccurateFields()
        {
            var fakeValueToReplaceOne = new FakeValueToReplace { }.Generate();
            var fakeValueToReplaceTwo = new FakeValueToReplace { }.Generate();

            var appFactory = _factory;
            using (var scope = appFactory.Services.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<ValueToReplaceDbContext>();
                context.Database.EnsureCreated();

                //context.ValueToReplaces.RemoveRange(context.ValueToReplaces);
                context.ValueToReplaces.AddRange(fakeValueToReplaceOne, fakeValueToReplaceTwo);
                context.SaveChanges();
            }

            var client = appFactory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });

            var result = await client.GetAsync($"api/ValueToReplaceLowers")
                .ConfigureAwait(false);
            var responseContent = await result.Content.ReadAsStringAsync()
                .ConfigureAwait(false);
            var response = JsonConvert.DeserializeObject<IEnumerable<ValueToReplaceDto>>(responseContent);

            // Assert
            result.StatusCode.Should().Be(200);
            response.Should().ContainEquivalentOf(fakeValueToReplaceOne, options =>
                options.ExcludingMissingMembers());
            response.Should().ContainEquivalentOf(fakeValueToReplaceTwo, options =>
                options.ExcludingMissingMembers());
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }

@HaoK
Copy link
Member

HaoK commented Aug 12, 2020

You are just going to have to go through things carefully, are you sure there isn't an auto included file somewhere you missed?

@pdevito3
Copy link
Author

Not that I'm seeing :-/ will try and dig into it deeper. thanks

@pranavkm
Copy link
Contributor

@pdevito3 feel free to reopen this if you have a consistent reproduction of the problem.

@ghost ghost locked as resolved and limited conversation to collaborators Oct 25, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-identity Includes: Identity and providers
Projects
None yet
Development

No branches or pull requests

4 participants