Search code examples
asp.netoauth-2.0.net-coreopeniddict

OpenIddict - hosting auth server and web api resource in same project


I want to implement an OpenIdConnect/Oauth2 server using OpenIddict in order to secure a .NET core API app. Most examples I have seen implement these as separate projects.

The client app is a SPA and we are using implicit flow.

I have based my solution on the code shown in the OpenIddict samples here: https://github.com/openiddict/openiddict-samples

For the project I am working on it would ideally have the Auth server and API to use the same port and be in the same project. ( One of the customer's requirements is that they don't want another server to configure since they own the API resource and it will be on the same server)

I have configured OpenIddict and combined it with our API project. Almost everything works correctly - the API endpoints are protected with the [Authorize] attribute and prevent access to protected API end points. However, when the API resource is protected, instead of returning a 401 Unauthorized HTTP status code, the returned result is the HTML Login page of the Auth server itself.

Here is the relevant setup code in my Startup.cs file:

 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseApplicationInsightsRequestTelemetry();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseApplicationInsightsExceptionTelemetry();

        app.UseStaticFiles();

        app.UseIdentity();

        app.UseCors("AllowAll");
        //app.UseCors(builder =>
        //{
        //    builder.AllowAnyOrigin();//)WithOrigins("http://localhost:9000");
        //    builder.WithMethods("GET","POST", "PUT", "DELETE", "OPTIONS");
        //    builder.WithHeaders("Authorization");
        //});

        app.UseWhen(context => !context.Request.Path.StartsWithSegments("/api"), branch =>
        {
            branch.UseIdentity();
        });

        app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), branch =>
        {
            branch.UseOAuthValidation();

        });

        app.UseOpenIddict();


        #region Adding resource config here (api)
        // Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715

        app.UseOAuthIntrospection(options =>
        {
            options.AutomaticAuthenticate = true;
            options.AutomaticChallenge = true;
            options.Authority = "http://localhost:5000";
            options.Audiences.Add("resource-server-1");
            options.ClientId = "resource-server-1";
            options.ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342";
        });

        //app.UseCors(builder => {
        //    builder.WithOrigins("http://localhost:9000");
        //    builder.WithMethods("GET");
        //    builder.WithHeaders("Authorization");
        //});
        #endregion


        app.UseMvcWithDefaultRoute();

        // Seed the database with the sample applications.
        // Note: in a real world application, this step should be part of a setup script.
        InitializeAsync(app.ApplicationServices, CancellationToken.None).GetAwaiter().GetResult();

    }

private async Task InitializeAsync(IServiceProvider services, CancellationToken cancellationToken)
    {
        // Create a new service scope to ensure the database context is correctly disposed when this methods returns.
        using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            //await context.Database.EnsureCreatedAsync();

            var manager = scope.ServiceProvider.GetRequiredService<OpenIddictApplicationManager<OpenIddictApplication>>();

            if (await manager.FindByClientIdAsync("MySPA", cancellationToken) == null)
            {
                var application = new OpenIddictApplication
                {
                    ClientId = "MySPA",
                    DisplayName = "MySPA",
                    LogoutRedirectUri = "http://localhost:9000/signout-oidc",
                    RedirectUri = "http://localhost:9000/signin-oidc"
                };

                await manager.CreateAsync(application, cancellationToken);
            }

            if (await manager.FindByClientIdAsync("resource-server-1", cancellationToken) == null)
            {
                var application = new OpenIddictApplication
                {
                    ClientId = "resource-server-1"
                };

                await manager.CreateAsync(application, "846B62D0-DEF9-4215-A99D-86E6B8DAB342", cancellationToken);
            }

        }
    }

Not sure how to implement these both side by side in the same project. As mentioned it all "works" except the API is returning the HTML login page and not a desired HTTP status


Solution

  • app.UseIdentity(); is present twice in your pipeline, which defeats the whole purpose of using branch.UseIdentity() in a app.UseWhen() branching builder (i.e making sure the cookies middleware registered by Identity are not invoked for your API endpoints).

    Remove the first occurrence and it should work.