Swagger is a useful tool for creating basic, on the fly API documentation using a standard JSON format that can be presented using a developer-friendly UI. These UIs typically allow you to start making demo requests via the browser. However, once you start protecting this API using OAuth, how do you keep this Swagger documentation functional?
Swagger integration with OAuth authorization servers is relatively well documented, so in this article, you’re going to see 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.
Preparing your API
To start, you will need to protect your API using IdentityServer. You can do this using either the JWT authentication handler from Microsoft or the IdentityServer specific implementation. I prefer the IdentityServer library, as this gives you some extra features, such as token introspection support, and saves you from having to perform some ceremony.
dotnet add package IdentityServer4.AccessTokenValidation
You can register this authentication library in your ConfigureServices
method:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication("Bearer", options =>
{
// required audience of access tokens
options.ApiName = "api1";
// auth server base endpoint (this will be used to search for disco doc)
options.Authority = "https://localhost:5000";
});
And then enable it in your HTTP pipeline by updating your Configure
method to look something like:
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
}
You can then 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
You can model this API in IdentityServer using the following ApiResource
and ApiScope
. With this approach, you are just using a single scope that signifies full access. You are welcome to create finer-grained access.
new ApiScope("api1", "Full access to API #1");
new ApiResource("api1", "API #1") {Scopes = {"api1"}};
To configure the Swagger UI as a client application in your IdentityServer implementation, you’ll need to add a client entry within IdentityServer that looks something like the following. You’re using the authorization code flow, PKCE, and a redirect URI with a path of /oauth2-redirect.html
, which is the default path for the Swagger UI. If you have a base path for your Swagger UI, then also include it in your redirect URI (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",
ClientSecrets = {new Secret("secret".Sha256())}, // change me!
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = {"https://localhost:5001/swagger/oauth2-redirect.html"},
AllowedCorsOrigins = {"https://localhost:5001"},
AllowedScopes = {"api1"}
}
Adding OAuth Support to Swashbuckle
Back in your API, let’s bring in Swashbuckle:
dotnet add package Swashbuckle.AspNetCore
dotnet add package Swashbuckle.AspNetCore.Swagger
Which you can register the by adding the following to your ConfigureServices
method:
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo {Title = "Protected API", Version = "v1"});
// we're going to be adding more here...
});
This configures a basic Swagger document with some descriptive info.
Next, you want to add some information about your authorization server to the swagger document. Since your 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, you’re going to use the authorization code flow (and later, Proof-Key for Code Exchange (PKCE)).
As a result, you’ll need to tell Swashbuckle the location of your authorization and token endpoints (check your IdentityServer disco doc), and what scopes it will be using (where the key is the scope itself, and the value is a display name).
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("https://localhost:5000/connect/authorize"),
TokenUrl = new Uri("https://localhost:5000/connect/token"),
Scopes = new Dictionary<string, string>
{
{"api1", "Demo API - full access"}
}
}
}
});
You’ll now need to tell your swagger document which endpoints require an access token to work, and that they can return 401 and 403 responses. You 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(OpenApiOperation operation, OperationFilterContext context)
{
var hasAuthorize =
context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any()
|| context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (hasAuthorize)
{
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[
new OpenApiSecurityScheme {Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"}
}
] = new[] {"api1"}
}
};
}
}
}
Here you are looking for all controllers and actions that have an AuthorizeAttribute
on them, and telling your Swagger document to include the extra possible responses, and that it needs an access token authorized with a specific scope, as defined in the security definition.
You can then register this like so:
options.OperationFilter<AuthorizeCheckOperationFilter>();
You can now configure both the Swagger document endpoint and the Swagger UI in your pipeline by adding the following to your 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");
options.OAuthUsePkce();
});
Here you are also stating the client ID you want the Swagger UI to use for authorization requests, a display name, and that it should use PKCE.
You can find a more comprehensive walkthrough of configuring Swashbuckle in your ASP.NET Core API on the ASP.NET Core documentation.
Swagger Document SecuritySchemes
Using the above configuration, both Swashbuckle and NSwag will output the same securitySchemes
section in their swagger document:
"securitySchemes": {
"oauth2": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://localhost:5000/connect/authorize",
"tokenUrl": "https://localhost:5000/connect/token",
"scopes": {
"api1": "Demo API - full access"
}
}
}
}
}
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, you’re using OAuth, not OpenID Connect, and this means that you are missing out on a couple of security features you get when using OpenID Connect, but at least newer versions of the Swagger UI support PKCE.
Swagger has added some OpenID Connect support, but this is only for the OpenID Connect discovery document. Their main selling point is that supported scopes can therefore be loaded in automatically. This isn’t all that useful, especially in larger systems with many scopes for many APIs.
The above approach, however, is much better than using the Resource Owner Password Credentials grant type (the password grant type).
The other issue is that currently, your Swagger documentation is open to the world. This may be 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 you could require a user to be logged in before they see your Swagger UI. Or better yet, protect the underlying Swagger document using OAuth.
To learn more about how Swagger handles API authentication and authorization, check out their documentation.
Andriy Kravets is writer and experience .NET developer and like .NET for regular development. He likes to build cross-platform libraries/software with .NET.