ASP.NET Core Swagger UI Authorization using IdentityServer4

Identity Server

Swagger

Swagger is a useful tool for creating basic, on the fly API documentation via both a standard JSON format that can then be presented via a UI. These UI’s typically allow you to start making demo requests via the browser. However, once we start protecting our API using OAuth, how do we keep this Swagger documentation functional?

Swagger integration with OAuth authorization servers is relatively well documented, so in this article, we’re going to look at the basics of adding IdentityServer support to an ASP.NET Core API using Swagger and then look at the limitations of this approach and some alternatives that might be worth exploring.

This article will demo both Swashbuckle and NSwag. Feel free to skip to the one that is most relevant to you and then move on to the limitations & improvements section.

Preparing your API

First, we need to protect our API using IdentityServer. We’ll do this using the IdentitServer4 library, as this gives us some extra features and saves us from having to configure a few things.

install-package IdentityServer4.AccessTokenValidation

We can then register this authentication library in our ConfigureServices method:

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(options =>
    {
        // auth server base endpoint (will use to search for disco doc)
        options.Authority = "http://localhost:5000";
        options.ApiName = "demo_api"; // required audience of access tokens
        options.RequireHttpsMetadata = false; // dev only!
    });

And then add it to our pipeline in the Configure method with:

app.UseAuthentication();

And finally, we can trigger this by using the AuthorizeAttribute on an action or a controller. You should now be getting 401 Unauthorized from these protected endpoints.

IdentityServer Configuration

We can then model this API in IdentityServer using the following ApiResource. Here we are just using the single scope signifying full access. You are welcome to create finer-grained access.

new ApiResource("demo_api", "Demo API with Swagger");

Adding OAuth Support to Swashbuckle

First, let’s bring in some packages that we need to move forward:

install-package Swashbuckle.AspNetCore
install-package Swashbuckle.AspNetCore.Swagger

Now we can register the required dependencies by adding the following to our ConfigureServices method:

services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new Info {Title = "Protected API", Version = "v1"});

    // we're going to be adding more here...
});

This configures a basic Swagger document and gives it some descriptive info.

Next, we want to add our OAuth configuration to this document. Since our UI is going to be running in the end-user’s browser, and access tokens are going to be required by JavaScript running in that browser, we’re going to use the implicit flow. We also need to tell it the location of our authorization endpoint (check you IdentityServer disco doc), and what scopes it can request (where the key is the scope itself, and the value is a display name).

options.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
    Flow = "implicit",
    AuthorizationUrl = "http://localhost:5000/connect/authorize",
    Scopes = new Dictionary<string, string> { 
        { "demo_api", "Demo API - full access" } 
    }
});

We now need to tell our document which endpoints require an access token to work, and also the fact that they can return 401 and 403 responses. We can do this using an IOperationFilter, which you can see below (this has been adapted from the filter found in the eShopOnContainers example repository.

public class AuthorizeCheckOperationFilter : IOperationFilter {
    public void Apply(Operation operation, OperationFilterContext context) {
        var hasAuthorize = 
                context.ControllerActionDescriptor
                       .GetControllerAndActionAttributes(true)
                       .OfType<AuthorizeAttribute>()
                       .Any();

if (hasAuthorize) { operation.Responses.Add("401", new Response { Description = "Unauthorized" }); operation.Responses.Add("403", new Response { Description = "Forbidden" });
operation.Security = new List<IDictionary<string, IEnumerable<string>>> { new Dictionary<string, IEnumerable<string>> {{"oauth2", new[] {"demo_api"}}} }; } } }

Here we are looking for all controllers and actions that have an AuthorizeAttribute on them, and telling our Swagger document to include the extra possible responses, and that it needs an access token from our security definition, with a particular scope.

We can then register this like so:

options.OperationFilter<AuthorizeCheckOperationFilter>();

We can now configure both the Swagger document endpoint and our Swagger UI in our pipeline by adding the following to our Configure method:

app.UseSwagger();

app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
options.OAuthClientId("demo_api_swagger"); options.OAuthAppName("Demo API - Swagger"); });

Here we are also stating the client ID you want the Swagger UI to use for authorization requests, and also give that client a basic display name.

You can find a more comprehensive walkthrough of configuring Swasbuckle in your ASP.NET Core API on the ASP.NET Core documentation.

IdentityServer Configuration

To configure the Swagger UI as a client application in our IdentityServer installation, we need to add a client that looks something like the following. Here we are using the implicit flow, just our API scopes, and a redirect URI with a path of /oauth2-redirect.html, which is the default path for Swashbuckle. If you have a base path for your Swagger UI, then also include this (i.e. if you have the Swagger UI on /swagger, your redirect URI should be /swagger/oauth2-redirect.html).

new Client {
    ClientId = "demo_api_swagger",
    ClientName = "Swagger UI for demo_api",
    AllowedGrantTypes = GrantTypes.Implicit,
    AllowAccessTokensViaBrowser = true,
    RedirectUris = {"http://localhost:5001/oauth2-redirect.html"},
    AllowedScopes = {"demo_api"}
};

Adding OAuth Support to NSwag

First step is to bring in the NSwag library via nuget:

install-package NSwag.AspNetCore

Now we can add Swagger doc generation to our project by adding the registration to our ConfigureServices method.

services.AddSwagger();

We can then enable the Swagger UI and document in our pipeline by adding the following to our Configure method:

app.UseSwaggerUi(typeof(Startup).GetTypeInfo().Assembly, settings =>
{
    settings.GeneratorSettings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase;

// we're going to be adding more here... }

You can find a more comprehensive walkthrough of configuring NSwag in your ASP.NET Core API on the ASP.NET Core documentation.

Now we can add our OAuth configuration to our Swagger document. Since our UI is going to be running in the end-user’s browser, and access tokens are going to be required by JavaScript running in that browser, we’re going to use the implicit flow. We also need to tell it the location of our authorization endpoint (check your IdentityServer disco doc), and what scopes it can request (where the key is the scope itself and the value is a display name).

settings.GeneratorSettings.DocumentProcessors.Add(
    new SecurityDefinitionAppender("oauth2", new SwaggerSecurityScheme
    {
        Type = SwaggerSecuritySchemeType.OAuth2,
        Flow = SwaggerOAuth2Flow.Implicit,
        AuthorizationUrl = "http://localhost:5000/connect/authorize",
        Scopes = new Dictionary<string, string> {{"demo_api", "Demo API - full access"}}
    }));

To let NSwag understand which endpoints require an access token and add security scopes to the Swagger document, we can use the OperationSecurityScopeProcessor class to automatically scan all of our controllers and action for AuthorizationAttributes.

settings.GeneratorSettings.OperationProcessors.Add(new OperationSecurityScopeProcessor("oauth2"));

And then finally, we can let our UI know about our client ID along with a human-readable display name:

settings.OAuth2Client = new OAuth2ClientSettings
{
    ClientId = "demo_api_swagger",
    AppName = "Demo API - Swagger"
};

IdentityServer Configuration

To configure the Swagger UI as a client application in our IdentityServer installation, we need to add a client that looks something like the following. Here we are using the implicit flow, just our API scopes, and a redirect URI with a path of /o2c.html, which is the default path for NSwag. If you have a base path for your Swagger UI, then also include this (i.e., if you have the Swagger UI on /swagger, your redirect URI should be /swagger/o2c.html).

new Client
{
    ClientId = "demo_api_swagger",
    ClientName = "Swagger UI for demo_api",
    AllowedGrantTypes = GrantTypes.Implicit,
    AllowAccessTokensViaBrowser = true,
    RedirectUris = {"http://localhost:5001/o2c.html"},
    AllowedScopes = { "demo_api" }
}

Limitations and Improvements

Okay, so the previous configuration shouldn’t be that new to you if you’ve used one of those libraries before or used OAuth before, so let’s add some value and see what sucks about the above configuration and what could be done to improve it.

For one, we’re using OAuth, not OpenID Connect, and this means that we’re missing out on a couple of security features we get when using OpenID Connect. When using the implicit flow and OAuth, we just get an access token back, and unfortunately, there’s not much we as a client application can do to verify that this was the token that was intended for us. Sure, we have the state parameter, but this can only prevent tokens being sent to us unexpectedly and is itself not verifiable proof.

When using OpenID Connect, if we request an identity token and an access token at the same time, the identity token is not only verifiable as the one intended to you thanks to the nonce parameter, but it will also include a hash of the access token. This way you have proof that the access token was intended to be used by your client application and there’s no one injecting tokens into your app. Hopefully Swagger will soon have inbuilt support for OpenID Connect.

The above approach, however, is much better than using the Resource Owner Password Credentials grant type (the password grant type). Don’t use that.

The other issue is that currently, our Swagger documentation is open to the world. This is fine for applications inside the company network or maybe for development apps, but I wouldn’t expect my private API to be documenting itself to the world. Why make it easy for them, right? So, maybe we could require a user to be logged in before they see our Swagger UI. Or better yet, protect our Swagger document using OAuth.

Source Code

Source code for the above examples using Swashbuckle, NSwag, and IdentityServer 4 is available on GitHub.

Scott Brady

Scott Brady

Scott Brady is the Identity & Access Control Lead at Rock Solid Knowledge, focusing on authorization & authentication protocols such as OAuth and OpenID Connect.

Keep Up To Date

Sign up to the mailing list to keep up to date with the latest articles and announcements.

Follow