From 1332212d47023c5d79c3304ef1bcc6d872ca8d3c Mon Sep 17 00:00:00 2001 From: domaindrivendev Date: Mon, 18 Jul 2022 15:46:06 +0100 Subject: [PATCH 1/2] Make infer security schemes opt-in --- .../SwaggerGenOptionsExtensions.cs | 17 +++ .../SwaggerGenerator/SwaggerGenerator.cs | 43 ++++--- .../SwaggerGeneratorOptions.cs | 29 +---- .../FakeAuthenticationSchemeProvider.cs | 47 ++++++++ .../SwaggerGenerator/SwaggerGeneratorTests.cs | 114 ++++++------------ 5 files changed, 131 insertions(+), 119 deletions(-) create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeAuthenticationSchemeProvider.cs diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs index 3d41d1c2c5..2239084555 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.OpenApi.Models; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Swashbuckle.AspNetCore.SwaggerGen; +using Microsoft.AspNetCore.Authentication; namespace Microsoft.Extensions.DependencyInjection { @@ -308,6 +309,22 @@ public static void SupportNonNullableReferenceTypes(this SwaggerGenOptions swagg swaggerGenOptions.SchemaGeneratorOptions.SupportNonNullableReferenceTypes = true; } + /// + /// Automatically infer security schemes from authentication/authorization state in ASP.NET Core. + /// + /// + /// + /// Provide alternative implementation that maps ASP.NET Core Authentication schemes to Open API security schemes + /// + /// Currently only supports JWT Bearer authentication + public static void InferSecuritySchemes( + this SwaggerGenOptions swaggerGenOptions, + Func, IDictionary> securitySchemesSelector = null) + { + swaggerGenOptions.SwaggerGeneratorOptions.InferSecuritySchemes = true; + swaggerGenOptions.SwaggerGeneratorOptions.SecuritySchemesSelector = securitySchemesSelector; + } + /// /// Extend the Swagger Generator with "filters" that can modify Schemas after they're initially generated /// diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index 8fe62db4a4..d54369f191 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -34,9 +34,9 @@ public SwaggerGenerator( SwaggerGeneratorOptions options, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, ISchemaGenerator schemaGenerator, - IAuthenticationSchemeProvider authentiationSchemeProvider) : this(options, apiDescriptionsProvider, schemaGenerator) + IAuthenticationSchemeProvider authenticationSchemeProvider) : this(options, apiDescriptionsProvider, schemaGenerator) { - _authenticationSchemeProvider = authentiationSchemeProvider; + _authenticationSchemeProvider = authenticationSchemeProvider; } public async Task GetSwaggerAsync(string documentName, string host = null, string basePath = null) @@ -88,27 +88,34 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin return (applicableApiDescriptions, swaggerDoc, schemaRepository); } - private async Task> GetSecuritySchemes() + private async Task> GetSecuritySchemes() { - var securitySchemes = new Dictionary(_options.SecuritySchemes); - var authenticationSchemes = Enumerable.Empty(); - if (_authenticationSchemeProvider is not null) + if (!_options.InferSecuritySchemes) { - authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); + return new Dictionary(_options.SecuritySchemes); } - var securitySchemesFromSelector = _options.SecuritySchemesSelector(authenticationSchemes); - // Favor security schemes set via options over those generated - // from the selector. For the default selector, this effectively - // ends up favoring `Bearer` authentication types explicitly set - // by the user over those derived by the selector. - foreach (var securityScheme in securitySchemesFromSelector) + + var authenticationSchemes = (_authenticationSchemeProvider is not null) + ? await _authenticationSchemeProvider.GetAllSchemesAsync() + : Enumerable.Empty(); + + if (_options.SecuritySchemesSelector != null) { - if (!securitySchemes.ContainsKey(securityScheme.Key)) - { - securitySchemes.Add(securityScheme.Key, securityScheme.Value); - } + return _options.SecuritySchemesSelector(authenticationSchemes); } - return securitySchemes; + + // Default implementation, currently only supports JWT Bearer scheme + return authenticationSchemes + .Where(authScheme => authScheme.Name == "Bearer") + .ToDictionary( + (authScheme) => authScheme.Name, + (authScheme) => new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", // "bearer" refers to the header name here + In = ParameterLocation.Header, + BearerFormat = "Json Web Token" + }); } private IList GenerateServers(string host, string basePath) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs index b4ba1f9c4c..48183d3cea 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs @@ -20,7 +20,7 @@ public SwaggerGeneratorOptions() OperationIdSelector = DefaultOperationIdSelector; TagsSelector = DefaultTagsSelector; SortKeySelector = DefaultSortKeySelector; - SecuritySchemesSelector = DefaultSecuritySchemeSelector; + SecuritySchemesSelector = null; SchemaComparer = StringComparer.Ordinal; Servers = new List(); SecuritySchemes = new Dictionary(); @@ -45,6 +45,10 @@ public SwaggerGeneratorOptions() public Func SortKeySelector { get; set; } + public bool InferSecuritySchemes { get; set; } + + public Func, IDictionary> SecuritySchemesSelector { get; set;} + public bool DescribeAllParametersInCamelCase { get; set; } public List Servers { get; set; } @@ -63,8 +67,6 @@ public SwaggerGeneratorOptions() public IList DocumentFilters { get; set; } - public Func, Dictionary> SecuritySchemesSelector { get; set;} - private bool DefaultDocInclusionPredicate(string documentName, ApiDescription apiDescription) { return apiDescription.GroupName == null || apiDescription.GroupName == documentName; @@ -106,26 +108,5 @@ private string DefaultSortKeySelector(ApiDescription apiDescription) { return TagsSelector(apiDescription).First(); } - - private Dictionary DefaultSecuritySchemeSelector(IEnumerable schemes) - { - Dictionary securitySchemes = new(); -#if (NET6_0_OR_GREATER) - foreach (var scheme in schemes) - { - if (scheme.Name == "Bearer") - { - securitySchemes[scheme.Name] = new OpenApiSecurityScheme - { - Type = SecuritySchemeType.Http, - Scheme = "bearer", // "bearer" refers to the header name here - In = ParameterLocation.Header, - BearerFormat = "Json Web Token" - }; - } - } -#endif - return securitySchemes; - } } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeAuthenticationSchemeProvider.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeAuthenticationSchemeProvider.cs new file mode 100644 index 0000000000..bbe01e472a --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeAuthenticationSchemeProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; + +namespace Swashbuckle.AspNetCore.SwaggerGen.Test +{ + public class FakeAuthenticationSchemeProvider : IAuthenticationSchemeProvider + { + private readonly IEnumerable _authenticationSchemes; + + public FakeAuthenticationSchemeProvider(IEnumerable authenticationSchemes) + { + _authenticationSchemes = authenticationSchemes; + } + + public void AddScheme(AuthenticationScheme scheme) + => throw new NotImplementedException(); + public Task> GetAllSchemesAsync() + => Task.FromResult(_authenticationSchemes); + + public Task GetDefaultAuthenticateSchemeAsync() + => Task.FromResult(_authenticationSchemes.First()); + + public Task GetDefaultChallengeSchemeAsync() + => Task.FromResult(_authenticationSchemes.First()); + + public Task GetDefaultForbidSchemeAsync() + => Task.FromResult(_authenticationSchemes.First()); + + public Task GetDefaultSignInSchemeAsync() + => Task.FromResult(_authenticationSchemes.First()); + + public Task GetDefaultSignOutSchemeAsync() + => Task.FromResult(_authenticationSchemes.First()); + + public Task> GetRequestHandlerSchemesAsync() + => throw new NotImplementedException(); + + public Task GetSchemeAsync(string name) + => Task.FromResult(_authenticationSchemes.First()); + + public void RemoveScheme(string name) + => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 53699c4aae..de8015624b 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -13,8 +13,8 @@ using Swashbuckle.AspNetCore.TestSupport; using Xunit; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.AspNetCore.Authentication; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { @@ -1081,76 +1081,70 @@ public void GetSwagger_SupportsOption_SecuritySchemes() var document = subject.GetSwagger("v1"); - Assert.Equal(new[] { "basic", "Bearer" }, document.Components.SecuritySchemes.Keys); - } - - [Fact] - public async Task GetSwagger_SupportsSecuritySchemesSelector() - { - var subject = Subject( - apiDescriptions: new ApiDescription[] { }, - options: new SwaggerGeneratorOptions - { - SwaggerDocs = new Dictionary - { - ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } - }, - SecuritySchemesSelector = (schemes) => new Dictionary - { - ["basic"] = new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, Scheme = "basic" } - } - } - ); - - var document = await subject.GetSwaggerAsync("v1"); - - // Overrides the default set of [basic, bearer] with just [basic] Assert.Equal(new[] { "basic" }, document.Components.SecuritySchemes.Keys); } - [Fact] - public async Task GetSwagger_DefaultSecuritySchemeSelectorAddsBearerByDefault() + [Theory] + [InlineData(false, new string[] { })] + [InlineData(true, new string[] { "Bearer" })] + public async Task GetSwagger_SupportsOption_InferSecuritySchemes( + bool inferSecuritySchemes, + string[] expectedSecuritySchemeNames) + { var subject = Subject( apiDescriptions: new ApiDescription[] { }, + authenticationSchemes: new[] { + new AuthenticationScheme("Bearer", null, typeof(IAuthenticationHandler)), + new AuthenticationScheme("Cookies", null, typeof(IAuthenticationHandler)) + }, options: new SwaggerGeneratorOptions { SwaggerDocs = new Dictionary { ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } }, + InferSecuritySchemes = inferSecuritySchemes } ); var document = await subject.GetSwaggerAsync("v1"); - Assert.Equal(new[] { "Bearer" }, document.Components.SecuritySchemes.Keys); + Assert.Equal(expectedSecuritySchemeNames, document.Components.SecuritySchemes.Keys); } - [Fact] - public async Task GetSwagger_DefaultSecuritySchemesSelectorDoesNotOverrideBearer() + [Theory] + [InlineData(false, new string[] { })] + [InlineData(true, new string[] { "Bearer", "Cookies" })] + public async Task GetSwagger_SupportsOption_SecuritySchemesSelector( + bool inferSecuritySchemes, + string[] expectedSecuritySchemeNames) + { var subject = Subject( apiDescriptions: new ApiDescription[] { }, + authenticationSchemes: new[] { + new AuthenticationScheme("Bearer", null, typeof(IAuthenticationHandler)), + new AuthenticationScheme("Cookies", null, typeof(IAuthenticationHandler)) + }, options: new SwaggerGeneratorOptions { SwaggerDocs = new Dictionary { ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } }, - SecuritySchemes = new Dictionary - { - ["Bearer"] = new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, Scheme = "someSpecialOne" } - } + InferSecuritySchemes = inferSecuritySchemes, + SecuritySchemesSelector = (authenticationSchemes) => + authenticationSchemes + .ToDictionary( + (authScheme) => authScheme.Name, + (authScheme) => new OpenApiSecurityScheme()) } ); var document = await subject.GetSwaggerAsync("v1"); - var securityScheme = Assert.Single(document.Components.SecuritySchemes); - Assert.Equal("Bearer", securityScheme.Key); - Assert.Equal(SecuritySchemeType.ApiKey, securityScheme.Value.Type); - Assert.Equal("someSpecialOne", securityScheme.Value.Scheme); + Assert.Equal(expectedSecuritySchemeNames, document.Components.SecuritySchemes.Keys); } [Fact] @@ -1283,13 +1277,16 @@ public void GetSwagger_SupportsOption_DocumentFilters() Assert.Contains("ComplexType", document.Components.Schemas.Keys); } - private SwaggerGenerator Subject(IEnumerable apiDescriptions, SwaggerGeneratorOptions options = null) + private SwaggerGenerator Subject( + IEnumerable apiDescriptions, + SwaggerGeneratorOptions options = null, + IEnumerable authenticationSchemes = null) { return new SwaggerGenerator( options ?? DefaultOptions, new FakeApiDescriptionGroupCollectionProvider(apiDescriptions), new SchemaGenerator(new SchemaGeneratorOptions(), new JsonSerializerDataContractResolver(new JsonSerializerOptions())), - new TestAuthenticationSchemeProvider() + new FakeAuthenticationSchemeProvider(authenticationSchemes ?? Enumerable.Empty()) ); } @@ -1301,41 +1298,4 @@ private SwaggerGenerator Subject(IEnumerable apiDescriptions, Sw } }; } - - class TestAuthenticationSchemeProvider : IAuthenticationSchemeProvider - { - private readonly IEnumerable _authenticationSchemes = new AuthenticationScheme[] - { - new AuthenticationScheme("Bearer", null, typeof(IAuthenticationHandler)) - }; - - public void AddScheme(AuthenticationScheme scheme) - => throw new NotImplementedException(); - public Task> GetAllSchemesAsync() - => Task.FromResult(_authenticationSchemes); - - public Task GetDefaultAuthenticateSchemeAsync() - => Task.FromResult(_authenticationSchemes.First()); - - public Task GetDefaultChallengeSchemeAsync() - => Task.FromResult(_authenticationSchemes.First()); - - public Task GetDefaultForbidSchemeAsync() - => Task.FromResult(_authenticationSchemes.First()); - - public Task GetDefaultSignInSchemeAsync() - => Task.FromResult(_authenticationSchemes.First()); - - public Task GetDefaultSignOutSchemeAsync() - => Task.FromResult(_authenticationSchemes.First()); - - public Task> GetRequestHandlerSchemesAsync() - => throw new NotImplementedException(); - - public Task GetSchemeAsync(string name) - => Task.FromResult(_authenticationSchemes.First()); - - public void RemoveScheme(string name) - => throw new NotImplementedException(); - } } \ No newline at end of file From ddd06271b21a23ece64494e2f2cf241ebd1396b1 Mon Sep 17 00:00:00 2001 From: domaindrivendev Date: Mon, 18 Jul 2022 15:59:13 +0100 Subject: [PATCH 2/2] Ensure document filters can affect inferred security schemes --- .../SwaggerGenerator/SwaggerGenerator.cs | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index d54369f191..d20fbf83a5 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -41,19 +41,41 @@ public SwaggerGenerator( public async Task GetSwaggerAsync(string documentName, string host = null, string basePath = null) { - var (applicableApiDescriptions, swaggerDoc, schemaRepository) = GetSwaggerDocument(documentName, host, basePath); + var (applicableApiDescriptions, swaggerDoc, schemaRepository) = GetSwaggerDocumentWithoutFilters(documentName, host, basePath); + swaggerDoc.Components.SecuritySchemes = await GetSecuritySchemes(); + + // NOTE: Filter processing moved here so they may effect generated security schemes + var filterContext = new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository); + foreach (var filter in _options.DocumentFilters) + { + filter.Apply(swaggerDoc, filterContext); + } + + swaggerDoc.Components.Schemas = new SortedDictionary(swaggerDoc.Components.Schemas, _options.SchemaComparer); + return swaggerDoc; } public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null) { - var (applicableApiDescriptions, swaggerDoc, schemaRepository) = GetSwaggerDocument(documentName, host, basePath); + var (applicableApiDescriptions, swaggerDoc, schemaRepository) = GetSwaggerDocumentWithoutFilters(documentName, host, basePath); + swaggerDoc.Components.SecuritySchemes = GetSecuritySchemes().Result; + + // NOTE: Filter processing moved here so they may effect generated security schemes + var filterContext = new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository); + foreach (var filter in _options.DocumentFilters) + { + filter.Apply(swaggerDoc, filterContext); + } + + swaggerDoc.Components.Schemas = new SortedDictionary(swaggerDoc.Components.Schemas, _options.SchemaComparer); + return swaggerDoc; } - private (IEnumerable, OpenApiDocument, SchemaRepository) GetSwaggerDocument(string documentName, string host = null, string basePath = null) + private (IEnumerable, OpenApiDocument, SchemaRepository) GetSwaggerDocumentWithoutFilters(string documentName, string host = null, string basePath = null) { if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info)) throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key)); @@ -77,14 +99,6 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin SecurityRequirements = new List(_options.SecurityRequirements) }; - var filterContext = new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository); - foreach (var filter in _options.DocumentFilters) - { - filter.Apply(swaggerDoc, filterContext); - } - - swaggerDoc.Components.Schemas = new SortedDictionary(swaggerDoc.Components.Schemas, _options.SchemaComparer); - return (applicableApiDescriptions, swaggerDoc, schemaRepository); }