openiddict / openiddict-core

Flexible and versatile OAuth 2.0/OpenID Connect stack for .NET
https://openiddict.com/
Apache License 2.0
4.43k stars 520 forks source link

Add a Sample where the Authorization Server and Resource Server are separate. #1340

Closed auxon closed 3 years ago

auxon commented 3 years ago

I am trying to figure out in ASP.NET Framework how to separate the Authorization Server and Resource Server. I want my Authorization Server to run on port https://localhost:5001 and authorize for apis hosted on another port. All the samples demo the resource server and authorization server as one thing. Is this a supported scenario?

kevinchalet commented 3 years ago

Zirku: implicit flow demo, with an Aurelia JS application acting as the client and two API projects using introspection (Api1) and local validation (Api2).

https://github.com/openiddict/openiddict-samples#aspnet-core-samples

auxon commented 3 years ago

Thanks for the quick response!

auxon commented 3 years ago

I am having problems because I am trying to get this to work for a .NET Framework 4.6.1 API. I found another older issue with the same problem. https://github.com/openiddict/openiddict-samples/issues/63# I am attempting to do the same thing. Except I'm using Client Credentials flow.

kevinchalet commented 3 years ago

If you need a hand, consider sponsoring the project and I'll make sure to point you in the right direction.

auxon commented 3 years ago

Done!

auxon commented 3 years ago

At first I was doing this from within my API Resource server:

        services.AddOpenIddict()
            .AddValidation(options =>
            {

                // Note: the validation handler uses OpenID Connect discovery
                // to retrieve the address of the introspection endpoint.
                options.SetIssuer("https://localhost:5001/");
                options.AddAudiences("SSHandlerServer");

                // Configure the validation handler to use introspection and register the client
                // credentials used when communicating with the remote introspection endpoint.
                options.UseIntrospection()
                       .SetClientId("SSHandlerServer")
                       .SetClientSecret("326CACD9-0EEA-4E58-9955-E4C4C3EDB5F5");

                // Register the System.Net.Http integration.
                options.UseSystemNetHttp();

                // Register the Owin host.
                options.UseOwin();
            });

It sounds like I can't do any of this with Owin, right?

auxon commented 3 years ago

Some problem trying to pay for the sponsorship. Getting a 404 when trying to use PayPal. I made a direct payment instead.

kevinchalet commented 3 years ago

Some problem trying to pay for the sponsorship. Getting a 404 when trying to use PayPal. I made a direct payment instead.

Thanks, much appreciated!

It sounds like I can't do any of this with Owin, right?

It should definitely work with OWIN. Do you use Web API 2 with the OWIN host? If so, did you decorate your API endpoints with [HostAuthentication(OpenIddictValidationOwinDefaults.AuthenticationType)]? Alternatively (but I wouldn't recommend it), you can call options.UseOwin().UseActiveAuthentication() to ensure the OpenIddict validation middleware provides an identity for all requests and converts 401 responses to proper challenges.

If it still doesn't work, please post your logs.

auxon commented 3 years ago

Yes, it's Web API 2.2 using Microsoft.AspNet.WebApi 5.2.7 which from what I found means Web API 2. I didn't decorate the API endpoint - I will try that! Thanks!

auxon commented 3 years ago

To be sure I am making sense, I am getting a token from my Authorization server via Postman and then making a Postman request to my .NET Framework 4.6.1 Web API 2 project API. Is Client Credential flow even right in this case?

kevinchalet commented 3 years ago

Without knowing more about your scenario, it's hard to tell. What's sure is that the client credentials flow is suited for machine-to-machine scenarios (i.e when there's no user involved and the client application needs to access API resources on its own).

auxon commented 3 years ago

Ok, that's what I want, M2M. Client credential flow seems correct. I am essentially trying to backport Zirku API 1 to ASP.NET Framework 4.6.1. I want to make sure I have configured things correctly in Startup.cs. I am starting over because my code is now a mess of things I've tried.

auxon commented 3 years ago

Stupid question - how to enable logs? I don't see anything except some debug output.

auxon commented 3 years ago

This is the code for Startup.cs in my Resource Server. The API I am hosting is called "scim".

`using Autofac; using Autofac.Extensions.DependencyInjection; using Autofac.Integration.WebApi; using Microsoft.Extensions.DependencyInjection; using Microsoft.Owin; using OpenIddict.Validation.Owin; using Owin; using Owin.Scim.Extensions; using SSHandlerServer.SCIM.CogniLore; using System; using System.IO; using System.Web.Http;

[assembly: OwinStartup(typeof(SSHandlerServer.Startup))]

namespace SSHandlerServer { public partial class Startup { public void Configuration(IAppBuilder appBuilder) { var container = CreateContainer(appBuilder);

        // Register the Autofac scope injector middleware.
        appBuilder.UseAutofacLifetimeScopeInjector(container);

        // Register the two OpenIddict server/validation middleware.
        appBuilder.UseMiddlewareFromContainer<OpenIddictValidationOwinMiddleware>();

        appBuilder.Map("/scim", app =>
        {
            app.UseScimServer(
                new Predicate<FileInfo>[] {
                    fileInfo => fileInfo.Name.Equals("SSHandlerServer.dll", StringComparison.OrdinalIgnoreCase)
                },
                config =>
                {
                    config.RequireSsl = true; // enable/disable SSL requirements
                    config.EnableEndpointAuthorization = true; // enable/disable authentication requirements
                    config.ModifyResourceType<CogniLoreUser>(b => b.SetValidator<CogniLoreUserValidator>());
                });
        });

        var configuration = new HttpConfiguration
        {
            DependencyResolver = new AutofacWebApiDependencyResolver(container)
        };

        configuration.MapHttpAttributeRoutes();

        // Register the Web API/Autofac integration middleware.
        appBuilder.UseAutofacWebApi(configuration);
        appBuilder.UseWebApi(configuration);

    }

    private IContainer CreateContainer(IAppBuilder appBuilder)
    {
        var services = new ServiceCollection();

        // Register the OpenIddict validation components.
        services.AddOpenIddict()
            .AddValidation(options =>
            {

                // Note: the validation handler uses OpenID Connect discovery
                // to retrieve the address of the introspection endpoint.
                options.SetIssuer("https://localhost:5001/");
                options.AddAudiences("SSHandlerServer");

                // Configure the validation handler to use introspection and register the client
                // credentials used when communicating with the remote introspection endpoint.
                options.UseIntrospection()
                       .SetClientId("SSHandlerServer")
                       .SetClientSecret("326CACD9-0EEA-4E58-9955-E4C4C3EDB5F5");

                // Register the System.Net.Http integration.
                options.UseSystemNetHttp();

                // Register the Owin host.
                options.UseOwin().UseActiveAuthentication();
            });

        var container = services.BuildServiceProvider();

        appBuilder.Use(async (context, next) =>
        {
            var scope = container.CreateScope();
            // Store the per-request service provider in the OWIN environment.
            context.Set(typeof(IServiceProvider).FullName, scope.ServiceProvider);
            try
            {
                // Invoke the rest of the pipeline.
                await next();
            }
            finally
            {
                // Remove the scoped service provider from the OWIN environment.
                context.Set<IServiceProvider>(typeof(IServiceProvider).FullName, null);
                // Dispose of the scoped service provider.
                if (scope is IAsyncDisposable disposable)
                {
                    await disposable.DisposeAsync();
                }
                else
                {
                    scope.Dispose();
                }
            }
        });
        appBuilder.UseOpenIddictValidation();

        // Create a new Autofac container and import the OpenIddict services.
        var builder = new ContainerBuilder();
        builder.Populate(services);

        return builder.Build();
    }
}

}`

auxon commented 3 years ago

I followed the instructions at https://documentation.openiddict.com/guide/getting-started.html to set up an ASP.NET Core Authorization Server I call "IdServer". This is the Startup.cs for IdServer now:

` using IdServer.Data; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks;

namespace IdServer { 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.AddControllersWithViews();

        services.AddDbContext<ApplicationDbContext>(options =>
        {
            // Configure the context to use Microsoft SQL Server.
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));

            // Register the entity sets needed by OpenIddict.
            // Note: use the generic overload if you need
            // to replace the default OpenIddict entities.
            options.UseOpenIddict();
        });
        services.AddLogging(logging =>
        {
            logging.AddDebug();
            logging.AddConsole();

        });
        services.AddOpenIddict()

            // Register the OpenIddict core components.
            .AddCore(options =>
            {
                // Configure OpenIddict to use the Entity Framework Core stores and models.
                // Note: call ReplaceDefaultEntities() to replace the default entities.
                options.UseEntityFrameworkCore()
                               .UseDbContext<ApplicationDbContext>();
            })

            // Register the OpenIddict server components.
            .AddServer(options =>
            {

                // Enable the token endpoint.
                options.SetTokenEndpointUris("/connect/token");

                // Enable the client credentials flow.
                options.AllowClientCredentialsFlow();

                // Register the signing and encryption credentials.
                options.AddDevelopmentEncryptionCertificate()
                    .AddDevelopmentSigningCertificate();

                // Register Scopes
                options.RegisterScopes("scim");

                // Register the ASP.NET Core host and configure the ASP.NET Core options.
                options.UseAspNetCore()
                    .EnableTokenEndpointPassthrough();
            })

            // Register the OpenIddict validation components.
            .AddValidation(options =>
            {
                // Import the configuration from the local OpenIddict server instance.
                options.UseLocalServer();

                // Register the ASP.NET Core host.
                options.UseAspNetCore();
            });

        // Register the worker responsible of seeding the database with the sample clients.
        // Note: in a real world application, this step should be part of a setup script.
        services.AddHostedService<Worker>();
        services.AddDatabaseDeveloperPageExceptionFilter();
        services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
        services.AddRazorPages();

    }

    // 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();
            app.UseMigrationsEndPoint();
        }
        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.UseRouting();

        app.UseCors(builder =>
        {
            builder.WithOrigins("https://localhost:44353");
            builder.WithMethods("GET");
            builder.WithHeaders("Authorization");
        });

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

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapDefaultControllerRoute();
            endpoints.MapRazorPages();
        });
    }
}

} `

auxon commented 3 years ago

This is my Worker.cs for the Authorization server:

using IdServer.Data; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenIddict.Abstractions; using System; using System.Threading; using System.Threading.Tasks; using static OpenIddict.Abstractions.OpenIddictConstants;

namespace IdServer {

public class Worker : IHostedService
{
    private readonly IServiceProvider _serviceProvider;

    public Worker(IServiceProvider serviceProvider)
        => _serviceProvider = serviceProvider;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        using var scope = _serviceProvider.CreateScope();

        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        await context.Database.EnsureCreatedAsync();

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

        if (await manager.FindByClientIdAsync("SSHandlerServer") is null)
        {
            await manager.CreateAsync(new OpenIddictApplicationDescriptor
            {
                ClientId = "SSHandlerServer",
                ClientSecret = "326CACD9-0EEA-4E58-9955-E4C4C3EDB5F5",
                DisplayName = "SSHandlerServer",
                Permissions =
            {
                Permissions.Endpoints.Token,
                Permissions.Endpoints.Introspection,
                Permissions.GrantTypes.ClientCredentials,
                Permissions.Prefixes.Scope + "scim"
            }
            });
        }
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

}

kevinchalet commented 3 years ago

Stupid question - how to enable logs? I don't see anything except some debug output.

OpenIddict uses the .NET Platform Extensions logging stack, so you can use any compatible listener and register it using the regular logging APIs:

E.g with Microsoft.Extensions.Logging.Debug:

services.AddLogging(options => options.AddDebug());
kevinchalet commented 3 years ago

This is the code for Startup.cs in my Resource Server. The API I am hosting is called "scim".

I wouldn't recommend using both Autofac AND a custom middleware to manually create scopes using the MSFT DI container. I don't think this explains why things are not working (but logging will definitely tell you what's happening) but you'll surely want to remove the custom DI stuff and use Autofac.

auxon commented 3 years ago

Yeah, there is a mix of Autofac because I was trying everything to work. I will clean that up. I set up logging for the Authorization Server (IdServer) and my Resource Server (SSHandlerServer) that hosts the API, thanks for that tip.

auxon commented 3 years ago

I will review your post step-by-step to reconfigure things with AutoFac: https://kevinchalet.com/2020/03/03/adding-openiddict-3-0-to-an-owin-application/

auxon commented 3 years ago

I got logging working so that should help. Thanks.

auxon commented 3 years ago

This is the Resource Server log when I start debugging. It's the only logging I get from the Resource server.

OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessRequestContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+InferIssuerFromHost. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromAuthorizationHeader. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromBodyForm. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromQueryString. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateToken. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was marked as rejected by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateToken. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessRequestContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+InferIssuerFromHost. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromAuthorizationHeader. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromBodyForm. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromQueryString. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateToken. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was marked as rejected by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateToken. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessRequestContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+InferIssuerFromHost. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromAuthorizationHeader. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromBodyForm. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers+ExtractAccessTokenFromQueryString. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateToken. OpenIddict.Validation.OpenIddictValidationDispatcher: Debug: The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was marked as rejected by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateToken.

auxon commented 3 years ago

This is the code for my Resource Server now:

`using Autofac; using Autofac.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Owin; using Owin; using Owin.Scim.Extensions; using System; using System.IO;

[assembly: OwinStartup(typeof(SSHandlerServer.Startup))]

namespace SSHandlerServer { public partial class Startup { public void Configuration(IAppBuilder appBuilder) { appBuilder.Map("/scim", app => { app.UseScimServer( new Predicate[] { fileInfo => fileInfo.Name.Equals("SSHandlerServer.dll", StringComparison.OrdinalIgnoreCase) }, configuration => { configuration.RequireSsl = true; // enable/disable SSL requirements configuration.EnableEndpointAuthorization = true; // enable/disable authentication requirements //configuration.ModifyResourceType(builder => builder.SetValidator()); }); });

        var services = new ServiceCollection();
        services.AddLogging(logging =>
        {
            logging.AddDebug();
            logging.SetMinimumLevel(LogLevel.Trace);
        });

        // Register the OpenIddict validation components.
        services.AddOpenIddict()
            .AddValidation(options =>
            {
                // Note: the validation handler uses OpenID Connect discovery
                // to retrieve the address of the introspection endpoint.
                options.SetIssuer("https://localhost:5001/");
                options.AddAudiences("SSHandlerServer");

                // Configure the validation handler to use introspection and register the client
                // credentials used when communicating with the remote introspection endpoint.
                options.UseIntrospection()
                       .SetClientId("SSHandlerServer")
                       .SetClientSecret("326CACD9-0EEA-4E58-9955-E4C4C3EDB5F5");

                // Register the System.Net.Http integration.
                options.UseSystemNetHttp();

                // Register the Owin host.
                options.UseOwin().UseActiveAuthentication();
            });

        var builder = new ContainerBuilder();
        builder.Populate(services);
        var container = builder.Build();
        appBuilder.UseAutofacMiddleware(container);
    }
}

}`

kevinchalet commented 3 years ago

It's weird you're not getting more details from the logging stack, like the error/error description returned by OI. Anyway, the ValidateToken handler does only one thing: ensuring a token was resolved.

https://github.com/openiddict/openiddict-core/blob/02c64fdd90d5d5d54e918b508088f3dce74233fb/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs#L84-L92

Are you 100% sure you're correctly attaching the token to the HTTP request?

auxon commented 3 years ago

image The request looks ok from Postman. I don't get any logging when making my request so something is wrong hooking things up.

kevinchalet commented 3 years ago

I don't get any logging when making my request so something is wrong hooking things up.

OWIN/Katana itself doesn't use the .NET Platform Extensions logging stuff, you need to configure that separately: https://github.com/aspnet/AspNetKatana/wiki/Debugging

Can you try attaching the token to the Authorization header to see if it makes any difference?

auxon commented 3 years ago

image Still the same issue. I tried enabling OWIN/Katana logging verbose. Debugging....

kevinchalet commented 3 years ago

Can you please try with a simple Web API 2 controller like the one in the sample project, just to be sure it's not something affected by SCIM?

kevinchalet commented 3 years ago

Also, try putting the appBuilder.Map("/scim", app => ...) stuff at the very end of your pipeline (at least, after appBuilder.UseAutofacMiddleware(container); to ensure OI is always invoked before the SCIM middleware)

auxon commented 3 years ago

That helped! More Logging happening ...

auxon commented 3 years ago

Yes, this is starting to work.

OpenIddict.Server.OpenIddictServerDispatcher: Information: The introspection request was rejected because the application 'SSHandlerServer' was not allowed to use the introspection endpoint. OpenIddict.Server.OpenIddictServerDispatcher: Information: The response was successfully returned as a JSON document: { "error": "unauthorized_client", "error_description": "This client application is not allowed to use the introspection endpoint.", "error_uri": "https://documentation.openiddict.com/errors/ID2075" }.

auxon commented 3 years ago

Ok, so I recreated the application in the database with the right permissions. Now it says

Information: The authentication demand was rejected because the token had no audience attached. I know what to do there I think.

auxon commented 3 years ago

It works!

auxon commented 3 years ago

Wow, thanks! I can't believe this is finally working.

kevinchalet commented 3 years ago

Good to hear (well, read 😄) that! 👏🏻