A sample environment check for OIDC authenticated web services

EDIT 8/25: Make sure you’re using Oakton 3.2.2 if you try to reproduce this sample, because of course I found and fixed an Oakton bug in this functionality in the process of writing the post:(

I think that what I’m calling an “environment check” here is a very valuable, and under utilized technique in software development today. I had a good reason to stop and create one today to troubleshoot an issue I stubbed my toe on, so here’s a blog post about environment checks.

One of the things on my plate at work right now is prototyping out the usage of Identity Server 5 as our new identity provider across our enterprise. Today I was working with a spike that needed to run a pair of ASP.Net Core applications:

  1. A web application that used the Duende.BFF library and JWT bearer tokens as its server side authentication mechanism.
  2. The actual identity server application

All I was trying to do was to run both applications and try to log into secure portions of the web application. No luck though, I was getting weird looking exceptions about missing configuration. I eventually debugged through the ASP.Net Core OIDC authentication innards, and found out that my issue was that the web application was configured with the wrong authority Url to the identity server service.

That turned out to be a very simple problem to fix, but the exception message was completely unrelated to the actual error, and that’s the kind of problem that’s unfortunately likely to come up again as this turns into a real solution that’s separately deployed in several different environments later (local development, CI, testing, staging, production, you know the drill).

As a preventative measure — or at least a diagnostic tool — I stopped and added an environment check into the application just to assert that yes, it can successfully reach the configured OIDC authority at the configured Url we expect it to be. Mechanically, I added Oakton as the command line runner for the application. After that, I utilized Oakton’s facility for adding and executing environment checks to a .Net Core or .Net 5+ application.

Inside the Startup.ConfigureServices() method for the main web application, I have this code to configure the server-side authentication (sorry, I know this is a lot of code):

            services.AddBff()
                .AddServerSideSessions();

            // This is specific to this application. This is just 
            // a cheap way of doing strong typed configuration
            // without using the ugly IOptions<T> stuff
            var settings = Configuration.Get<OidcSettings>();
            
            // configure server-side authentication and session management
            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "cookie";
                    options.DefaultChallengeScheme = "oidc";
                    options.DefaultSignOutScheme = "oidc";
                })
                .AddCookie("cookie", options =>
                {
                    // host prefixed cookie name
                    options.Cookie.Name = settings.CookieName;

                    // strict SameSite handling
                    options.Cookie.SameSite = SameSiteMode.Strict;
                })
                .AddOpenIdConnect("oidc", options =>
                {
                    // This is the root Url for the OIDC authority
                    options.Authority = settings.Authority;

                    // confidential client using code flow + PKCE + query response mode
                    options.ClientId = settings.ClientId;
                    options.ClientSecret = settings.ClientSecret;
                    options.ResponseType = "code";
                    options.ResponseMode = "query";
                    options.UsePkce = true;

                    options.MapInboundClaims = true;
                    options.GetClaimsFromUserInfoEndpoint = true;

                    // save access and refresh token to enable automatic lifetime management
                    options.SaveTokens = true;

                    // request scopes
                    options.Scope.Clear();
                    options.Scope.Add("openid");
                    options.Scope.Add("profile");
                    options.Scope.Add("api");

                    // request refresh token
                    options.Scope.Add("offline_access");
                });

The main thing I want you to note in the code above is that the Url for the OIDC identity service is bound from the raw configuration to the OidcSettings.Authority property. Now that we know where the authority Url is, formulating an environment check is this code, again within the Startup.ConfigureServices() method:

            // This extension method registers an environment check with Oakton
            services.CheckEnvironment<HttpClient>($"Is the expected OIDC authority at {settings.Authority} running", async (client, token) =>
            {
                var document = await client.GetDiscoveryDocumentAsync(settings.Authority, cancellationToken: token);
                
                if (document == null)
                {
                    throw new Exception("Unable to load the OIDC discovery document. Is the OIDC authority server running?");
                }
                else if (document.IsError)
                {
                    throw document.Exception;
                }
            });

Now, if the authority Url is misconfigured, or the OIDC identity service is not running, I can quickly diagnose that issue by running this command from the main web application directory:

dotnet run -- check-env

If I were to run this command with the identity service is down, I’ll get this lovely output:

Stop, dogs, stop! The light is red!

Hopefully, I can tell from the description and the exception message that the identity service that is supposed to be at https://localhost:5010 is not responsive. In this case it was because I had purposely turned it off to show you the failure, so let’s turn the identity service on and try again with dotnet run -- check-env:

Go, dogs, go! It’s green ahead!

As part of turning this work over to real developers to turn this work into real, production worthy code, we’ll document the dotnet run -- check-env tooling for detecting errors. More effectively though, with Oakton the command line call will return a non-zero error code if the environment checks fail, so it’s useful to run this command in automated deployments as a “fail fast” check on the environment that can point to specifically where the application is misconfigured or external dependencies are unreachable.

As another alternative, Oakton can run these environment checks for you at application startup time by using the -c or --check flag when the application executable is executed.

Leave a comment