5 minute read

How to secure the OpenApi schema and Scalar browser in a Microsoft Asp Net Core Web Api using Microsoft Identity Web for authentication.

Intro

In my previous post I showed how to setup OpenApi and Scalar in a Microsoft Asp Net Core Web Api. In this post I will show how to secure the OpenApi schema and Scalar browser using Microsoft Identity Web for authentication.

The OpenApi schema and Scalar browser can contain sensitive information about your API, such as the available endpoints, full documentation, request and response schemas, and authentication requirements.

Microsoft recommends OpenApi enabled only in development environments. But securing the OpenApi schema allows you to safely expose these resources in production environments as well, without risking unintended information disclosure to unauthorized users.

Prerequisites

A dotnet Asp Net Core Web Api running .NET 10.

Existing authorization setup with OAuth2 (I’m using Microsoft.Identity.Web in this guide)

Install these nuget packages:

  • Scalar.AspNetCore (I used v2.12.41) (Note, alternatives like swagger-ui may also work.)
  • Microsoft.AspNetCore.OpenApi (I used v10.0.3)

Setting up OpenApi and Scalar

See the guide in my previous post: Microsoft OpenApi Setup with Scalar

Authentication configuration

Assuming your website has setup authentication with Microsoft Identity Web, your setup looks something like this:

builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi(
        DownstreamApiService.DownstreamApiServiceName,
        builder.Configuration.GetSection(
            $"DownstreamApis:{DownstreamApiConfig.DownstreamApiName}"
        )
    )
    .AddInMemoryTokenCaches();

// Snip ...

var app = builder.Build();
// Snip ...

app.UseAuthentication();
app.UseAuthorization();
// Snip ...

app.MapOpenApi();
app.MapScalarApiReference("/scalar", options => ...);
// Snip ...

Adding multiple Authentication Schemes

As the WebApi authentication setup is designed for APIs, it uses the JwtBearer authentication scheme by default. This is not suitable for authenticating users in the browser, as it does not support interactive login flows. To authenticate users in the browser, we need to use the OpenIdConnect authentication scheme, which is designed for web applications and supports interactive login flows. This is easily added by adding the AddMicrosoftIdentityWebAppAuthentication method to the services, which adds the OpenIdConnect authentication scheme.

First out:

Add a callbackPath to your authentication setup.

// in your appsettings.json
"AzureAd": {
  // Snip other AzureAd config
  "CallbackPath": "/signin-oidc",
}

You may have to add redirect URIs in your Azure AD app registration for the OpenIdConnect authentication to work. The redirect URI should be in the format https://<your-api-base-url>/signin-oidc.

Then add the AddMicrosoftIdentityWebAppAuthentication (NOTE: App, not Api) to your services:

builder.Services.AddMicrosoftIdentityWebApiAuthentication(
    // Snip (see above)
)

// Note: IdentityWebApp, not Api! Otherwise identical to the Api setup (You may not require downstream api setup here though, depending on your use-case)
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi(
        DownstreamApiService.DownstreamApiServiceName,
        builder.Configuration.GetSection(
            $"DownstreamApis:{DownstreamApiConfig.DownstreamApiName}"
        )
    )
    .AddInMemoryTokenCaches();

We now have two authentication schemes, one for the API and one for the Web App. The Web App scheme is used by Scalar to authenticate users in the browser, and the API scheme is used to authenticate requests to the API.

We want to use the Api authentication for most routes, so we setup a default authentication scheme:

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(
            OpenIdConnectDefaults.AuthenticationScheme, // Optional: Allows us to use the OpenIdConnect cookie if authenticated
            JwtBearerDefaults.AuthenticationScheme // This is the default scheme used for API authentication
        )
        .RequireAuthenticatedUser()
        .Build();
});

Now we setup the Scalar Auth Policy: This has the OpenIdConnect scheme as default, which will cause it to redirect to the Azure AD login page when authentication is required.

var scalarUiAuthenticationPolicy = new AuthorizationPolicyBuilder()
    .AddAuthenticationSchemes(
        JwtBearerDefaults.AuthenticationScheme,
        OpenIdConnectDefaults.AuthenticationScheme // Intentionally use OpenIdConnect last as it will redirect to Azure AD login page.
    )
    .AddRequirements(new DenyAnonymousAuthorizationRequirement())
    .Build();

Then we have to setup the OpenApi schema to use the Web App authentication scheme, and require the correct scopes.

Add “RequireAuthorization” to the OpenApi endpoint to require authentication for the OpenApi document itself. This uses the “default” authentication scheme, which we set to the API scheme above.

app.MapOpenApi().RequireAuthorization(scalarUiAuthenticationPolicy);

Finally we add the RequireAuthorization to the Scalar endpoint

app.MapScalarApiReference(options =>
    {
        // Snip Scalar setup (see previous post)
    })
    .RequireAuthorization(scalarUiAuthenticationPolicy);

Fin

Good!

Now you should be able to run the API locally and see that a login is required to access the OpenApi document and the Scalar UI.

End

// Nils Henrik

This post was written by a human.