OpenIddict server with ClientCertificateMode.RequireCertificate and .well-known/openid-configuration endpoint

Viewed 355

I have Openiddict secured with client certificates /smart cards and have RequireCertificate. The Resource (api) Server isn't able to hit the .well-known/openid-configuration endpoint since it does not have a client cert. The endpoints are secured with Openiddict. With AllowCertificate then it all works but I need to use RequireCertificate.

I tried using just local validation but it still calls to the Auth Server

Is there a way to not have the Resource server reach out to the Auth Server for the .well-known/openid-configuration endpoint or can I host the .well-known/openid-configuration endpoint on the Resource Server?

//startup.cs
   public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();         
            
            services.AddDbContext<DbContext>(options =>
            {
                // Configure the context to use Microsoft SQL Server.
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly("OpeniddictServerWindows"));

                // Register the entity sets needed by OpenIddict.
                // Note: use the generic overload if you need
                // to replace the default OpenIddict entities.
                options.UseOpenIddict();
            });


            // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
            // (like pruning orphaned authorizations/tokens 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.AddAuthentication(
                   CertificateAuthenticationDefaults.AuthenticationScheme)
                   .AddCertificate(options =>
                   {
                       options.AllowedCertificateTypes = CertificateTypes.All;
                       options.ValidateCertificateUse = false;

                       options.Events = new CertificateAuthenticationEvents
                       {
                           OnAuthenticationFailed = context =>
                           {
                               context.Fail("failed");
                               return Task.CompletedTask;
                           },

                           OnCertificateValidated = context =>
                           {
                               //TODO: validat cert if not expired and whatever else                           

                               var cc = context.ClientCertificate;

                               var claims = new[]
                               {
                                    new Claim(
                                        ClaimTypes.NameIdentifier,
                                        context.ClientCertificate.Subject,
                                        ClaimValueTypes.String,
                                        context.Options.ClaimsIssuer),

                                    new Claim(ClaimTypes.Name,
                                        context.ClientCertificate.Subject,
                                        ClaimValueTypes.String,
                                        context.Options.ClaimsIssuer)
                               };

                               context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));

                               context.Success();

                               return Task.CompletedTask;
                           }


                       };
                   });


            services.AddCors(options =>
            {
                options.AddPolicy("AllowAllOrigins",
                    builder =>
                    {
                        builder
                            .AllowCredentials()
                            .WithOrigins(
                                "http://localhost:4200"

                                )
                            .SetIsOriginAllowedToAllowWildcardSubdomains()
                            .AllowAnyHeader()
                            .AllowAnyMethod();
                    });
            });



            // Configure Identity to use the same JWT claims as OpenIddict instead
            // of the legacy WS-Federation claims it uses by default (ClaimTypes),
            // which saves you from doing the mapping in your authorization controller.
            services.Configure<IdentityOptions>(options =>
            {
                options.ClaimsIdentity.UserNameClaimType = Claims.Name;
                options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
                options.ClaimsIdentity.RoleClaimType = Claims.Role;
            });


            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<DbContext>();

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

               // Register the OpenIddict server components.
               .AddServer(options =>
               {
                 
                   
                   //Enable the authorization and token endpoints.
                   options.SetAuthorizationEndpointUris("/connect/authorize")
                          //.SetIntrospectionEndpointUris("/connect/introspect")
                          .SetTokenEndpointUris("/connect/token")
                          .SetUserinfoEndpointUris("/connect/userinfo");

                   // Mark the "email", "profile", "roles" and "dataEventRecords" scopes as supported scopes.
                   options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "api1");


                   // Note: this sample only uses the authorization code flow but you can enable
                   // the other flows if you need to support implicit, password or client credentials.
                   options.AllowAuthorizationCodeFlow()
                          .AllowRefreshTokenFlow();


                   // Register the encryption credentials. This sample uses a symmetric
                   // encryption key that is shared between the server and the Api2 sample
                   // (that performs local token validation instead of using introspection).
                   //
                   // Note: in a real world application, this encryption key should be
                   // stored in a safe place (e.g in Azure KeyVault, stored as a secret).
                   //options.AddEncryptionKey(new SymmetricSecurityKey(
                   //    Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY=")));

                   var cert = CertificateUtility.LoadFromFile("C:\\certs\\AuthServerCert.pfx", "password");
                   options.AddEncryptionCertificate(cert);

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


                   //used for local validation
                   //options.AddEncryptionKey(new SymmetricSecurityKey(
                   //    Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY=")));




                   // Register the signing credentials.
                   options.AddDevelopmentSigningCertificate();


                   // Force client applications to use Proof Key for Code Exchange (PKCE).
                   options.RequireProofKeyForCodeExchange();

                   // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
                   //
                   // Note: unlike other samples, this sample doesn't use token endpoint pass-through
                   // to handle token requests in a custom MVC action. As such, the token requests
                   // will be automatically handled by OpenIddict, that will reuse the identity
                   // resolved from the authorization code to produce access and identity tokens.
                   //
                   options.UseAspNetCore()
                          .EnableStatusCodePagesIntegration()
                          .EnableAuthorizationEndpointPassthrough()
                          .EnableTokenEndpointPassthrough()
                          .EnableUserinfoEndpointPassthrough()
                          

                           // During development, you can disable the HTTPS requirement.
                           .DisableTransportSecurityRequirement();

                   options.IgnoreEndpointPermissions()
                           .IgnoreScopePermissions();


                   options.DisableAccessTokenEncryption();

                   

               })

               // 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>();


        }


//Program.cs
webBuilder.ConfigureKestrel(options =>
                    {
                        options.ConfigureHttpsDefaults(q =>
                        {
                            q.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
                            q.SslProtocols = SslProtocols.Tls12;

                        });
                    });

1 Answers

A few general pointers here:

REQUIRE CLIENT CERTS

Your approach is blocking all clients and forcing them all to supply a client cert, which is clearly no good. OAuth with Mutual TLS usually requires a dedicated port or hostname separate to that used by other clients - see this Curity article for an example setup.

CONSIDER A REVERSE PROXY

A standard hosting setup is to put a reverse proxy in front of the Authorization Server. You can then use the RP for other things:

See this financial grade article for some common scenarios where mutual TLS is terminated at the reverse proxy. Of course you should ensure that clients that must authenticate via Mutual TLS cannot somehow bypass it via a different URL.

AUTHORIZATION SERVER

I don't like the amount of code you are having to write. An AS should require zero code by default - it should just be a Docker container you spin up that exposes endpoints.

Have a look at these resources for an example of spinning up an out of the box AS and RP. Note also the use of custom logic in the RP:

Related