Skip to content

Commit

Permalink
Add authentication support in Swagger for .NET 9.0+
Browse files Browse the repository at this point in the history
Introduce new functionality for adding authentication support in Swagger for .NET 9.0 or greater. This includes configuring and displaying the Authorize button in the Swagger UI for JWT Bearer, API Key, and Basic Authentication.

Encapsulate changes within `#if NET9_0_OR_GREATER` preprocessor directives to ensure compatibility with .NET 9.0+.

Add new using directives for necessary namespaces.

Introduce `SwaggerExtensions` class in `OpenApiExtensions.cs` with `AddSimpleAuthentication` methods for configuring Swagger authentication.

Add `AuthenticationDocumentTransformer` class in `AuthenticationDocumentTransformer.cs` to transform OpenAPI documents with security definitions and requirements.

Add `AuthenticationOperationTransformer` class in `AuthenticationOperationTransformer.cs` to automatically add 401 and 403 responses to authorized operations.

Introduce `Helpers` class in `Helpers.cs` with utility methods for creating security requirements and responses.

Ensure Swagger UI supports various authentication schemes for easier testing of authenticated endpoints.
  • Loading branch information
marcominerva committed Dec 11, 2024
1 parent 42f9978 commit 9ba1a77
Show file tree
Hide file tree
Showing 4 changed files with 292 additions and 0 deletions.
109 changes: 109 additions & 0 deletions src/SimpleAuthentication/OpenApi/AuthenticationDocumentTransformer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#if NET9_0_OR_GREATER

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Configuration;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using SimpleAuthentication.ApiKey;
using SimpleAuthentication.BasicAuthentication;
using SimpleAuthentication.JwtBearer;

namespace SimpleAuthentication.OpenApi;

internal class AuthenticationDocumentTransformer(IConfiguration configuration, string sectionName, params IEnumerable<OpenApiSecurityRequirement> additionalSecurityRequirements) : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
// Adds a security definition for each authentication method that has been configured.
CheckAddJwtBearer(document, configuration.GetSection($"{sectionName}:JwtBearer"));
CheckAddApiKey(document, configuration.GetSection($"{sectionName}:ApiKey"));
CheckAddBasicAuthentication(document, configuration.GetSection($"{sectionName}:Basic"));

if (additionalSecurityRequirements?.Any() ?? false)
{
// Adds all the other security requirements that have been specified.
foreach (var securityRequirement in additionalSecurityRequirements)
{
AddSecurityRequirement(document, securityRequirement);
}
}

return Task.CompletedTask;

static void CheckAddJwtBearer(OpenApiDocument document, IConfigurationSection section)
{
var settings = section.Get<JwtBearerSettings>();
if (settings is null)
{
return;
}

AddSecurityScheme(document, settings.SchemeName, SecuritySchemeType.Http, JwtBearerDefaults.AuthenticationScheme, ParameterLocation.Header, HeaderNames.Authorization, "Insert the Bearer Token");
AddSecurityRequirement(document, settings.SchemeName);
}

static void CheckAddApiKey(OpenApiDocument document, IConfigurationSection section)
{
var settings = section.Get<ApiKeySettings>();
if (settings is null)
{
return;
}

if (!string.IsNullOrWhiteSpace(settings.HeaderName))
{
AddSecurityScheme(document, $"{settings.SchemeName} in Header", SecuritySchemeType.ApiKey, null, ParameterLocation.Header, settings.HeaderName, "Insert the API Key");
AddSecurityRequirement(document, $"{settings.SchemeName} in Header");
}

if (!string.IsNullOrWhiteSpace(settings.QueryStringKey))
{
AddSecurityScheme(document, $"{settings.SchemeName} in Query String", SecuritySchemeType.ApiKey, null, ParameterLocation.Query, settings.QueryStringKey, "Insert the API Key");
AddSecurityRequirement(document, $"{settings.SchemeName} in Query String");
}
}

static void CheckAddBasicAuthentication(OpenApiDocument document, IConfigurationSection section)
{
var settings = section.Get<BasicAuthenticationSettings>();
if (settings is null)
{
return;
}

AddSecurityScheme(document, settings.SchemeName, SecuritySchemeType.Http, BasicAuthenticationDefaults.AuthenticationScheme, ParameterLocation.Header, HeaderNames.Authorization, "Insert user name and password");
AddSecurityRequirement(document, settings.SchemeName);
}
}

private static void AddSecurityScheme(OpenApiDocument document, string name, SecuritySchemeType securitySchemeType, string? scheme, ParameterLocation location, string parameterName, string description)
{
document.Components ??= new();
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>();

document.Components.SecuritySchemes.Add(name, new()
{
In = location,
Name = parameterName,
Description = description,
Type = securitySchemeType,
Scheme = scheme,
//Reference = new()
//{
// Id = name,
// Type = ReferenceType.SecurityScheme
//}
});
}
private static void AddSecurityRequirement(OpenApiDocument document, string name)
=> AddSecurityRequirement(document, Helpers.CreateSecurityRequirement(name));

private static void AddSecurityRequirement(OpenApiDocument document, OpenApiSecurityRequirement requirement)
{
document.SecurityRequirements ??= [];
document.SecurityRequirements.Add(requirement);
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#if NET9_0_OR_GREATER

using System.Net;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using SimpleAuthentication.ApiKey;
using SimpleAuthentication.BasicAuthentication;
using SimpleAuthentication.JwtBearer;

namespace SimpleAuthentication.OpenApi;

internal class AuthenticationOperationTransformer(IAuthorizationPolicyProvider authorizationPolicyProvider) : IOpenApiOperationTransformer
{
public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
// If the method requires authorization, automatically add 401 and 403 response (if not explicitly specified).
var fallbackPolicy = await authorizationPolicyProvider.GetFallbackPolicyAsync();
var requireAuthenticatedUser = fallbackPolicy?.Requirements.Any(r => r is DenyAnonymousAuthorizationRequirement) ?? false;

var endpointMetadata = context.Description.ActionDescriptor.EndpointMetadata;

var requireAuthorization = endpointMetadata.Any(m => m is AuthorizeAttribute);
var allowAnonymous = endpointMetadata.Any(m => m is AllowAnonymousAttribute);

if ((requireAuthenticatedUser || requireAuthorization) && !allowAnonymous)
{
operation.Responses.TryAdd(StatusCodes.Status401Unauthorized.ToString(), Helpers.CreateResponse(HttpStatusCode.Unauthorized.ToString()));
operation.Responses.TryAdd(StatusCodes.Status403Forbidden.ToString(), Helpers.CreateResponse(HttpStatusCode.Forbidden.ToString()));
}
}
}

#endif
52 changes: 52 additions & 0 deletions src/SimpleAuthentication/OpenApi/Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#if NET9_0_OR_GREATER

using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;

namespace SimpleAuthentication.OpenApi;

internal static class Helpers
{
public static OpenApiSecurityRequirement CreateSecurityRequirement(string name)
{
var requirement = new OpenApiSecurityRequirement()
{
{
new()
{
Reference = new()
{
Type = ReferenceType.SecurityScheme,
Id = name
}
},
[]
}
};

return requirement;
}

public static OpenApiResponse CreateResponse(string description)
=> new()
{
Description = description,
Content = new Dictionary<string, OpenApiMediaType>
{
[MediaTypeNames.Application.ProblemJson] = new()
{
Schema = new()
{
Reference = new()
{
Type = ReferenceType.Schema,
Id = nameof(ProblemDetails)
}
}
}
}
};
}

#endif
89 changes: 89 additions & 0 deletions src/SimpleAuthentication/OpenApiExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#if NET9_0_OR_GREATER

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using SimpleAuthentication.ApiKey;
using SimpleAuthentication.BasicAuthentication;
using SimpleAuthentication.JwtBearer;
using SimpleAuthentication.OpenApi;

namespace SimpleAuthentication;

/// <summary>
/// Provides extension methods for adding authentication support in Swagger.
/// </summary>
public static class SwaggerExtensions
{
/// <summary>
/// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from the specified <see cref="IConfiguration"/> source.
/// </summary>
/// <param name="options">The <see cref="OpenApiOptions"/> to add configuration to.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> being bound.</param>
/// <param name="sectionName">The name of the configuration section that holds authentication settings (default: Authentication).</param>
/// <seealso cref="OpenApiOptions"/>
/// <seealso cref="IConfiguration"/>
public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, string sectionName = "Authentication")
=> options.AddSimpleAuthentication(configuration, sectionName, Array.Empty<OpenApiSecurityRequirement>());

/// <summary>
/// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from a section named <strong>Authentication</strong> in <see cref="IConfiguration"/> source.
/// </summary>
/// <param name="options">The <see cref="OpenApiOptions"/> to add configuration to.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> being bound.</param>
/// <param name="additionalSecurityDefinitionNames">The name of additional security definitions that have been defined in Swagger.</param>
/// <seealso cref="OpenApiOptions"/>
/// <seealso cref="IConfiguration"/>
public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, params IEnumerable<string> additionalSecurityDefinitionNames)
=> options.AddSimpleAuthentication(configuration, "Authentication", additionalSecurityDefinitionNames);

/// <summary>
/// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from the specified <see cref="IConfiguration"/> source.
/// </summary>
/// <param name="options">The <see cref="OpenApiOptions"/> to add configuration to.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> being bound.</param>
/// <param name="sectionName">The name of the configuration section that holds authentication settings (default: Authentication).</param>
/// <param name="additionalSecurityDefinitionNames">The name of additional security definitions that have been defined in Swagger.</param>
/// <seealso cref="OpenApiOptions"/>
/// <seealso cref="IConfiguration"/>
public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, string sectionName, params IEnumerable<string> additionalSecurityDefinitionNames)
{
var securityRequirements = additionalSecurityDefinitionNames?.Select(Helpers.CreateSecurityRequirement).ToArray();
options.AddSimpleAuthentication(configuration, sectionName, securityRequirements ?? []);
}

/// <summary>
/// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from the specified <see cref="IConfiguration"/> source.
/// </summary>
/// <param name="options">The <see cref="OpenApiOptions"/> to add configuration to.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> being bound.</param>
/// <param name="securityRequirements">Additional security requirements to be added to Swagger definition.</param>
/// <seealso cref="OpenApiOptions"/>
/// <seealso cref="IConfiguration"/>
public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, params IEnumerable<OpenApiSecurityRequirement> securityRequirements)
=> options.AddSimpleAuthentication(configuration, "Authentication", securityRequirements);

/// <summary>
/// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from the specified <see cref="IConfiguration"/> source.
/// </summary>
/// <param name="options">The <see cref="OpenApiOptions"/> to add configuration to.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> being bound.</param>
/// <param name="sectionName">The name of the configuration section that holds authentication settings (default: Authentication).</param>
/// <param name="additionalSecurityRequirements">Additional security requirements to be added to Swagger definition.</param>
/// <seealso cref="OpenApiOptions"/>
/// <seealso cref="IConfiguration"/>
public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, string sectionName, params IEnumerable<OpenApiSecurityRequirement> additionalSecurityRequirements)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(sectionName);

options.AddDocumentTransformer(new AuthenticationDocumentTransformer(configuration, sectionName, additionalSecurityRequirements));
options.AddOperationTransformer<AuthenticationOperationTransformer>();
}
}

#endif

0 comments on commit 9ba1a77

Please sign in to comment.