OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.43k stars 2.4k forks source link

OpenIDDict as custom Identity implementation #13041

Closed dewebeloper closed 5 months ago

dewebeloper commented 1 year ago

I'm trying ti implement Decouplecd CMS with OpenIdDict identity implementation. According to https://github.com/OrchardCMS/OrchardCore/issues/3900 that is possible.

Idea is to use custom identity for registered user and OC users just to decoupled CMS.

I use OpenIdDict example from https://github.com/openiddict/openiddict-core/tree/dev/sandbox and just followed Creating a new decoupled CMS Website.

On the end I finished with

 public void ConfigureServices(IServiceCollection services)
    {
        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.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        })

        .AddCookie( options =>
        {
            options.LoginPath = "/login";
            options.LogoutPath = "/logout";
            options.ExpireTimeSpan = TimeSpan.FromMinutes(50);
            options.SlidingExpiration = false;
        });

        services.AddAuthorization(options => {
            var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme);
            defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
            options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
        });

        // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
        // (like pruning orphaned authorizations from the database) at regular intervals.
        services.AddQuartz(options =>
        {
            options.UseMicrosoftDependencyInjectionJobFactory();
            options.UseSimpleTypeLoader();
            options.UseInMemoryStore();
        });

        // Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
        services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);

        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 OpenIddict entities.
                options.UseEntityFrameworkCore()
                       .UseDbContext<ApplicationDbContext>();

                // Developers who prefer using MongoDB can remove the previous lines
                // and configure OpenIddict to use the specified MongoDB database:
                // options.UseMongoDb()
                //        .UseDatabase(new MongoClient().GetDatabase("openiddict"));

                // Enable Quartz.NET integration.
                options.UseQuartz();
            })

            // Register the OpenIddict client components.
            .AddClient(options =>
            {
                // Note: this sample uses the authorization code and refresh token
                // flows, but you can enable the other flows if necessary.
                options.AllowAuthorizationCodeFlow()
                       .AllowRefreshTokenFlow();

                // Register the signing and encryption credentials used to protect
                // sensitive data like the state tokens produced by OpenIddict.
                options.AddDevelopmentEncryptionCertificate()
                       .AddDevelopmentSigningCertificate();

                // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
                options.UseAspNetCore()
                       .EnableStatusCodePagesIntegration()
                       .EnableRedirectionEndpointPassthrough()
                       .EnablePostLogoutRedirectionEndpointPassthrough();

                // Register the System.Net.Http integration and use the identity of the current
                // assembly as a more specific user agent, which can be useful when dealing with
                // providers that use the user agent as a way to throttle requests (e.g Reddit).
                options.UseSystemNetHttp()
                       .SetProductInformation(typeof(Startup).Assembly);

                // Add a client registration matching the client application definition in the server project.
                options.AddRegistration(new OpenIddictClientRegistration
                {
                    Issuer = new Uri("https://localhost:44395/", UriKind.Absolute),
                    ProviderName = "Local",

                    ClientId = "mvc",
                    ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
                    Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },

                    RedirectUri = new Uri("callback/login/local", UriKind.Relative),
                    PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative)
                });

                // Register the Web providers integrations.
                //
                // Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
                // URI per provider, unless all the registered providers support returning a special "iss"
                // parameter containing their URL as part of authorization responses. For more information,
                // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
                options.UseWebProviders()
                       .UseGitHub(options =>
                       {
                           options.SetClientId("c4ade52327b01ddacff3")
                                  .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122")
                                  .SetRedirectUri("callback/login/github");
                       })
                       .UseGoogle(options =>
                       {
                           options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com")
                                  .SetClientSecret("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")
                                  .SetRedirectUri("callback/login/google")
                                  .SetAccessType("offline")
                                  .AddScopes(Scopes.Profile);
                       })
                       .UseReddit(options =>
                       {
                           options.SetClientId("vDLNqhrkwrvqHgnoBWF3og")
                                  .SetClientSecret("Tpab28Dz0upyZLqn7AN3GFD1O-zaAw")
                                  .SetRedirectUri("callback/login/reddit")
                                  .SetDuration("permanent");
                       })
                       .UseTwitter(options =>
                       {
                           options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
                                  .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
                                  .SetRedirectUri("callback/login/twitter")
                                  .AddScopes("offline.access");
                       });
            });

        services.AddHttpClient();

        services.AddControllersWithViews();
        services.AddOrchardCms();

        // Register the worker responsible for creating the database used to store tokens.
        // Note: in a real world application, this step should be part of a setup script.
        services.AddHostedService<Worker>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();

        app.UseStaticFiles();

        app.UseStatusCodePagesWithReExecute("/error");

        app.UseRouting();

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

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

          app.UseOrchardCore();
    }

And this approach works fine with OpenIDDIct. I was able to redirect to OpenIdDict server and login and get data from resource controller. However, there is issue when I try to go to /admin in order to access OC admin page. There I get

AmbiguousMatchException: The request matched multiple endpoints. Matches:

OrchardCore.Email.Controllers.AdminController.Index (OrchardCore.Email)
OrchardCore.Cors.Controllers.AdminController.Index (OrchardCore.Cors)
OrchardCore.Apis.GraphQL.Controllers.AdminController.Index (OrchardCore.Apis.GraphQL)
OrchardCore.AuditTrail.Controllers.AdminController.Index (OrchardCore.AuditTrail)
OrchardCore.ContentTypes.Controllers.AdminController.Index (OrchardCore.ContentTypes)
OrchardCore.Layers.Controllers.AdminController.Index (OrchardCore.Layers)
OrchardCore.Media.Controllers.AdminController.Index (OrchardCore.Media)
OrchardCore.Placements.Controllers.AdminController.Index (OrchardCore.Placements)
OrchardCore.Queries.Controllers.AdminController.Index (OrchardCore.Queries)
OrchardCore.Search.Elasticsearch.AdminController.Index (OrchardCore.Search.Elasticsearch)
OrchardCore.Search.Lucene.Controllers.AdminController.Index (OrchardCore.Search.Lucene)
OrchardCore.Recipes.Controllers.AdminController.Index (OrchardCore.Recipes)
OrchardCore.Roles.Controllers.AdminController.Index (OrchardCore.Roles)
OrchardCore.Tenants.Controllers.AdminController.Index (OrchardCore.Tenants)
OrchardCore.Shortcodes.Controllers.AdminController.Index (OrchardCore.Shortcodes)
OrchardCore.Users.Controllers.AdminController.Index (OrchardCore.Users)
OrchardCore.Themes.Controllers.AdminController.Index (OrchardCore.Themes)
OrchardCore.Settings.Controllers.AdminController.Index (OrchardCore.Settings)
OrchardCore.Admin.Controllers.AdminController.Index (OrchardCore.Admin)

Anybody has idea what can be changed in order to make this works?

sebastienros commented 1 year ago

Looks like you are trying to configure opendiddict like for any other web app, even though you shouldn't have to do anything like this since the modules already do that for you.

Summoning @kevinchalet

sebastienros commented 1 year ago

@lahma what have you broken again? (options.UseQuartz();)

lahma commented 1 year ago

@lahma what have you broken again? (options.UseQuartz();)

Odd to see the library mentioned that I actually should concentrate on 🤪

dewebeloper commented 1 year ago

@sebastienros that configuration is best I can get. I tried to remove services.AddControllersWithViews(); and to remove

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

also tried to remove line byline, but in any way was not able to complete login operation.

kevinchalet commented 1 year ago

Looks like you are trying to configure opendiddict like for any other web app, even though you shouldn't have to do anything like this since the modules already do that for you.

I agree. Using OpenIddict in parallel to OrchardCore (instead of using the native module) should work, as long as you don't try to do anything OpenIddict-related in an OrchardCore module. No need so say it's not something I've tested, so... 😅

That said, it's not specific to OpenIddict: as soon as you register some complex stuff at the host level, it's likely you'll see some side effects affecting OrchardCore or its modules.

FWIW, I'm not sure the symptom you describe is caused/related to OpenIddict. It seems like an issue with the fact you're using app.UseEndpoints() + options.MapControllers() at the host level, which may import the OC controllers in the MVC parts but without the infrastructure types normally registered by OC (probably a MatcherPolicy in this specific case)... and cause such issues.

dewebeloper commented 1 year ago

@kevinchalet @sebastienros is there any other way (configuration) how I can achieve my scenario?

sebastienros commented 1 year ago

@dewebeloper

is there any other way (configuration) how I can achieve my scenario

I'm trying ti implement Decouplecd CMS with OpenIdDict identity implementation

Assuming you are trying to achieve the first statement, then look in the documentation on how to do it using the existing modules (OpenIdDict ones). I think there are also some blog posts external to Orchard showing it (links appreciated if someone has them).