4

I'm currently trying to setup an OpenIddict based AuthServer for my company. I'm currently struggling with accessing a secured endpoint from one of my test APIs.

Desired solution in the future

What I want to achieve in the long run: A centralized Auth-Server based on OpenIddict where my collegues can register their APIs/clients/softwareprojects for a secured access. For simplicity I call one of the APIs of my collegue API1. This API1 has a bunch of controllers and endpoints, which need securing using the Auth-Server. A couple of the endpoints need different protection levels. So Client1 of the API1 is getting the credentials only for the read endpoints, as opposed to Client2 who get's credentials for also the read and write endpoints.

Current situation and problem

Right now I'm trying to start with a simple example where I have the Auth-Server and a API1, the client is Postman for now.

I get a valid token using the OAuth2-method in Postman, here is the log of the Auth-Server:

    info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The request address matched a server endpoint: Token.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The token request was successfully extracted: {
        "grant_type": "client_credentials",
        "scope": "",
        "client_id": "resource_server_1",
        "client_secret": "[redacted]"
      }.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The token request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The response was successfully returned as a JSON document: {
        "access_token": "[redacted]",
        "token_type": "Bearer",
        "expires_in": 3599
      }.

But when I use this token in Postman to access my authorized endpoint in my API1 I get this error from API1:

    info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[12]
      AuthenticationScheme: OpenIddict.Validation.AspNetCore was challenged.
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[100]
      Start processing HTTP request GET https://localhost:5001/.well-known/openid-configuration
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[100]
      Sending HTTP request GET https://localhost:5001/.well-known/openid-configuration
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[101]
      Received HTTP response after 326.6051ms - OK
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[101]
      End processing HTTP request after 366.9958ms - OK
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[100]
      Start processing HTTP request GET https://localhost:5001/.well-known/jwks
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[100]
      Sending HTTP request GET https://localhost:5001/.well-known/jwks
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[101]
      Received HTTP response after 76.1564ms - OK
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[101]
      End processing HTTP request after 83.7518ms - OK
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[7]
      OpenIddict.Validation.AspNetCore was not authenticated. Failure message: An error occurred while authenticating the current request.
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[7]
      OpenIddict.Validation.AspNetCore was not authenticated. Failure message: An error occurred while authenticating the current request.
info: OpenIddict.Validation.OpenIddictValidationDispatcher[0]
      The response was successfully returned as a JSON document: {
        "error": "server_error",
        "error_description": "This resource server is currently unavailable.",
        "error_uri": "https://documentation.openiddict.com/errors/ID2092"
      }.
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[12]
      AuthenticationScheme: OpenIddict.Validation.AspNetCore was challenged.

The Auth-Server spits out this:

    info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The request address matched a server endpoint: Configuration.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The configuration request was successfully extracted: {}.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The configuration request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The response was successfully returned as a JSON document: {
        "issuer": "https://localhost:5001/",
        "token_endpoint": "https://localhost:5001/connect/token",
        "jwks_uri": "https://localhost:5001/.well-known/jwks",
        "grant_types_supported": [
          "client_credentials"
        ],
        "scopes_supported": [
          "openid",
          "scp:profile"
        ],
        "claims_supported": [
          "aud",
          "exp",
          "iat",
          "iss",
          "sub"
        ],
        "id_token_signing_alg_values_supported": [
          "RS256"
        ],
        "subject_types_supported": [
          "public"
        ],
        "token_endpoint_auth_methods_supported": [
          "client_secret_basic",
          "client_secret_post"
        ],
        "claims_parameter_supported": false,
        "request_parameter_supported": false,
        "request_uri_parameter_supported": false
      }.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The request address matched a server endpoint: Cryptography.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The cryptography request was successfully extracted: {}.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The cryptography request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The response was successfully returned as a JSON document: {
        "keys": [
          {
            "kid": "B0A4787E6E637564D164006A0E48F5C4FB1285BC",
            "use": "sig",
            "kty": "RSA",
            "alg": "RS256",
            "e": "AQAB",
            "n": "{VERY-LONG-STRING-HERE}",
            "x5t": "sKR4fm5jdWTRZABqDkj1xPsShbw",
            "x5c": [
              "{VERY-LONG-STRING-HERE}"
            ]
          }
        ]
      }.

Code

Here is my Startup from the Auth-Server:

    using Cece.Server.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Cece.Server
{
    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.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>();
                })

                // 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 the ASP.NET Core host and configure the ASP.NET Core-specific options.
                    options.UseAspNetCore()
                           .EnableTokenEndpointPassthrough();

                    options.RegisterScopes(OpenIddictConstants.Permissions.Scopes.Profile);
                })

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

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

            app.UseRouting();

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

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

            app.UseWelcomePage();
        }
    }
}

And the worker in the Auth-Server:

    using Cece.Server.Models;
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 Cece.Server
{
    internal 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();

            await CreateApplicationsAsync();
            await CreateScopesAsync();

            async Task CreateApplicationsAsync()
            {
                var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();

                if (await manager.FindByClientIdAsync("cece") == null)
                {
                    var descriptor = new OpenIddictApplicationDescriptor
                    {
                        ClientId = "cece",
                        DisplayName = "Cece client application",
                        Permissions =
                        {
                            Permissions.Endpoints.Authorization,
                            Permissions.Endpoints.Logout,
                            Permissions.GrantTypes.ClientCredentials,
                            Permissions.ResponseTypes.IdToken,
                            Permissions.ResponseTypes.IdTokenToken,
                            Permissions.ResponseTypes.Token,
                            Permissions.Scopes.Email,
                            Permissions.Scopes.Profile,
                            Permissions.Scopes.Roles,
                            Permissions.Prefixes.Scope + "api1"
                        }
                    };

                    await manager.CreateAsync(descriptor);
                }

                if (await manager.FindByClientIdAsync("resource_server_1") == null)
                {
                    var descriptor = new OpenIddictApplicationDescriptor
                    {
                        ClientId = "resource_server_1",
                        ClientSecret = "My-Secret",
                        Permissions =
                        {
                            Permissions.GrantTypes.ClientCredentials,
                            Permissions.Endpoints.Token,
                            Permissions.Endpoints.Introspection
                        }
                    };

                    await manager.CreateAsync(descriptor);
                }

                // Note: no client registration is created for resource_server_2
                // as it uses local token validation instead of introspection.
            }

            async Task CreateScopesAsync()
            {
                var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();

                if (await manager.FindByNameAsync("api1") == null)
                {
                    var descriptor = new OpenIddictScopeDescriptor
                    {
                        Name = "api1",
                        Resources =
                        {
                            "resource_server_1"
                        }
                    };

                    await manager.CreateAsync(descriptor);
                }
            }
        }

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

And here is my Startup from the API1:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenIddict.Validation.AspNetCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Cece.TestApi1
{
    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.AddAuthentication(options =>
            {
                options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
            });

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

                    // Configure the validation handler to use introspection and register the client
                    // credentials used when communicating with the remote introspection endpoint.
                    options.UseIntrospection()
                           .SetClientId("resource_server_1")
                           .SetClientSecret("My-Secret");

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

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

            services.AddControllers();
        }

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

            app.UseRouting();

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

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

Questions

So my questions are:

  1. Am I on the right track regarding my required setup described above?
  2. What exactly does this error I'm getting mean?
MiHo
  • 170
  • 7

1 Answers1

5

To be able to use introspection, you must enable the introspection endpoint by giving it an address in the server options. Fix that and this error should go away.

Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131