diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 928e9f0..240f23c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,7 +1,7 @@ ## General - Make only high confidence suggestions when reviewing code changes. -- Always use the latest version C#, currently C# 13 features. +- Always use the latest version C#, currently C# 14 features. - Write code that is clean, maintainable, and easy to understand. - Only add comments rarely to explain why a non-intuitive solution was used. The code should be self-explanatory otherwise. - Don't add the UTF-8 BOM to files unless they have non-ASCII characters. diff --git a/samples/Controllers/ApiKeySample/ApiKeySample.csproj b/samples/Controllers/ApiKeySample/ApiKeySample.csproj index 15b70ac..9eb2607 100644 --- a/samples/Controllers/ApiKeySample/ApiKeySample.csproj +++ b/samples/Controllers/ApiKeySample/ApiKeySample.csproj @@ -1,14 +1,14 @@  - net9.0 + net10.0 enable enable - - + + diff --git a/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj b/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj index 15b70ac..9eb2607 100644 --- a/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj +++ b/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj @@ -1,14 +1,14 @@  - net9.0 + net10.0 enable enable - - + + diff --git a/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj b/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj index 15b70ac..9eb2607 100644 --- a/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj +++ b/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj @@ -1,14 +1,14 @@  - net9.0 + net10.0 enable enable - - + + diff --git a/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj b/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj index 15b70ac..687d104 100644 --- a/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj +++ b/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj @@ -1,18 +1,17 @@  - net9.0 + net10.0 enable enable - - + + - diff --git a/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj b/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj index 15b70ac..687d104 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj +++ b/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj @@ -1,18 +1,17 @@  - net9.0 + net10.0 enable enable - - + + - diff --git a/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj b/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj index f98eec3..687d104 100644 --- a/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj +++ b/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj @@ -1,14 +1,14 @@  - net9.0 + net10.0 enable enable - - + + diff --git a/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj b/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj index 31d32fe..54fc3da 100644 --- a/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj +++ b/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/Net8JwtBearerSample/Program.cs b/samples/MinimalApis/Net8JwtBearerSample/Program.cs index bbd2d5b..789a2e2 100644 --- a/samples/MinimalApis/Net8JwtBearerSample/Program.cs +++ b/samples/MinimalApis/Net8JwtBearerSample/Program.cs @@ -2,6 +2,7 @@ using JwtBearerSample.Authentication; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.OpenApi; using SimpleAuthentication; using SimpleAuthentication.JwtBearer; using SimpleAuthentication.Permissions; @@ -57,7 +58,11 @@ app.UseExceptionHandler(); app.UseStatusCodePages(); -app.UseSwagger(); +app.UseSwagger(options => +{ + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; +}); + app.UseSwaggerUI(); app.UseAuthentication(); @@ -79,11 +84,7 @@ var token = await jwtBearerService.CreateTokenAsync(loginRequest.UserName, claims, absoluteExpiration: expiration); return TypedResults.Ok(new LoginResponse(token)); }) -.WithOpenApi(operation => -{ - operation.Description = "Insert permissions in the scope property (for example: 'profile people:admin')"; - return operation; -}); +.WithDescription("Insert permissions in the scope property (for example: 'profile people:admin')"); authApiGroup.MapPost("validate", async Task, BadRequest>> (string token, bool validateLifetime, IJwtBearerService jwtBearerService) => { @@ -95,15 +96,13 @@ } return TypedResults.Ok(new User(result.Principal.Identity!.Name)); -}) -.WithOpenApi(); +}); authApiGroup.MapPost("refresh", async (string token, bool validateLifetime, DateTime? expiration, IJwtBearerService jwtBearerService) => { var newToken = await jwtBearerService.RefreshTokenAsync(token, validateLifetime, expiration); return TypedResults.Ok(new LoginResponse(newToken)); -}) -.WithOpenApi(); +}); app.MapGet("api/me", (ClaimsPrincipal user) => { @@ -111,22 +110,14 @@ }) .RequireAuthorization() .RequirePermission("profile") -.WithOpenApi(operation => -{ - operation.Description = "This endpoint requires the 'profile' permission"; - return operation; -}); +.WithDescription("This endpoint requires the 'profile' permission"); app.MapGet("api/people", () => { return TypedResults.NoContent(); }) .RequireAuthorization(policyNames: "PeopleRead") -.WithOpenApi(operation => -{ - operation.Description = $"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions"; - return operation; -}); +.WithDescription($"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions"); app.Run(); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 979fdda..18d1785 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -9,7 +9,7 @@ - + diff --git a/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj b/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj index eea5a73..d05f562 100644 --- a/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj +++ b/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable latest @@ -28,11 +28,15 @@ - + - + + + + + diff --git a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj index 3210d6f..ac43c14 100644 --- a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj +++ b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable latest @@ -31,11 +31,6 @@ - - - - - True @@ -43,5 +38,13 @@ + + + + + + + + diff --git a/src/SimpleAuthentication.Swashbuckle/Swagger/AuthenticationOperationFilter.cs b/src/SimpleAuthentication.Swashbuckle/Swagger/AuthenticationOperationFilter.cs index 4b9a451..d23df59 100644 --- a/src/SimpleAuthentication.Swashbuckle/Swagger/AuthenticationOperationFilter.cs +++ b/src/SimpleAuthentication.Swashbuckle/Swagger/AuthenticationOperationFilter.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.AspNetCore.Http; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace SimpleAuthentication.Swagger; diff --git a/src/SimpleAuthentication.Swashbuckle/Swagger/OpenApiHelpers.cs b/src/SimpleAuthentication.Swashbuckle/Swagger/OpenApiHelpers.cs index 2af5e61..1110fed 100644 --- a/src/SimpleAuthentication.Swashbuckle/Swagger/OpenApiHelpers.cs +++ b/src/SimpleAuthentication.Swashbuckle/Swagger/OpenApiHelpers.cs @@ -1,25 +1,15 @@ using System.Net.Mime; using Microsoft.AspNetCore.Mvc; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; namespace SimpleAuthentication.Swagger; internal static class OpenApiHelpers { - public static OpenApiSecurityRequirement CreateSecurityRequirement(string name) + public static OpenApiSecurityRequirement CreateSecurityRequirement(string name, OpenApiDocument document) => new() { - { - new() - { - Reference = new() - { - Type = ReferenceType.SecurityScheme, - Id = name - } - }, - [] - } + { new OpenApiSecuritySchemeReference(name, document), [] } }; public static OpenApiResponse CreateResponse(string description) @@ -30,7 +20,7 @@ public static OpenApiResponse CreateResponse(string description) { [MediaTypeNames.Application.ProblemJson] = new() { - Schema = new() + Schema = new OpenApiSchemaReference(nameof(ProblemDetails)) { Reference = new() { diff --git a/src/SimpleAuthentication.Swashbuckle/Swagger/ProblemDetailsDocumentFilter.cs b/src/SimpleAuthentication.Swashbuckle/Swagger/ProblemDetailsDocumentFilter.cs index c844773..0aab884 100644 --- a/src/SimpleAuthentication.Swashbuckle/Swagger/ProblemDetailsDocumentFilter.cs +++ b/src/SimpleAuthentication.Swashbuckle/Swagger/ProblemDetailsDocumentFilter.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace SimpleAuthentication.Swagger; diff --git a/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs b/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs index e20dc20..79db6e0 100644 --- a/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs +++ b/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using SimpleAuthentication.ApiKey; using SimpleAuthentication.BasicAuthentication; using SimpleAuthentication.JwtBearer; @@ -25,7 +25,7 @@ public static class SwaggerExtensions /// /// public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, string sectionName = "Authentication") - => options.AddSimpleAuthentication(configuration, sectionName, Array.Empty()); + => options.AddSimpleAuthentication(configuration, sectionName, [], []); /// /// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from a section named Authentication in source. @@ -35,7 +35,7 @@ public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConf /// The name of additional security definitions that have been defined in Swagger. /// /// - public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, IEnumerable? additionalSecurityDefinitionNames) + public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, params IEnumerable additionalSecurityDefinitionNames) => options.AddSimpleAuthentication(configuration, "Authentication", additionalSecurityDefinitionNames); /// @@ -47,11 +47,7 @@ public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConf /// The name of additional security definitions that have been defined in Swagger. /// /// - public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, string sectionName, IEnumerable? additionalSecurityDefinitionNames) - { - var securityRequirements = additionalSecurityDefinitionNames?.Select(OpenApiHelpers.CreateSecurityRequirement).ToArray(); - options.AddSimpleAuthentication(configuration, sectionName, securityRequirements ?? []); - } + public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, string sectionName, params IEnumerable additionalSecurityDefinitionNames) => options.AddSimpleAuthentication(configuration, sectionName, [], additionalSecurityDefinitionNames); /// /// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from the specified source. @@ -61,8 +57,8 @@ public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConf /// Additional security requirements to be added to Swagger definition. /// /// - public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, params OpenApiSecurityRequirement[] securityRequirements) - => options.AddSimpleAuthentication(configuration, "Authentication", securityRequirements); + public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, params IEnumerable securityRequirements) + => options.AddSimpleAuthentication(configuration, "Authentication", securityRequirements, []); /// /// Adds authentication support in Swagger, enabling the Authorize button in the Swagger UI, reading configuration from the specified source. @@ -71,9 +67,10 @@ public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConf /// The being bound. /// The name of the configuration section that holds authentication settings (default: Authentication). /// Additional security requirements to be added to Swagger definition. + /// The name of additional security definitions that have been defined in OpenAPI. /// /// - public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, string sectionName, params OpenApiSecurityRequirement[] additionalSecurityRequirements) + public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConfiguration configuration, string sectionName, IEnumerable additionalSecurityRequirements, IEnumerable additionalSecurityDefinitionNames) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(configuration); @@ -84,12 +81,21 @@ public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConf CheckAddApiKey(options, configuration.GetSection($"{sectionName}:ApiKey")); CheckAddBasicAuthentication(options, configuration.GetSection($"{sectionName}:Basic")); - if (additionalSecurityRequirements?.Any() ?? false) + if (additionalSecurityRequirements?.Any() == true) { // Adds all the other security requirements that have been specified. foreach (var securityRequirement in additionalSecurityRequirements) { - options.AddSecurityRequirement(securityRequirement); + AddSecurityRequirement(options, securityRequirement); + } + } + + if (additionalSecurityDefinitionNames?.Any() == true) + { + // Adds all the other security definitions that have been specified. + foreach (var definitionName in additionalSecurityDefinitionNames) + { + AddSecurityRequirement(options, definitionName); } } @@ -146,7 +152,7 @@ static void CheckAddBasicAuthentication(SwaggerGenOptions options, IConfiguratio } static void AddSecurityDefinition(SwaggerGenOptions options, string name, SecuritySchemeType securitySchemeType, string? scheme, ParameterLocation location, string parameterName, string description) - => options.AddSecurityDefinition(name, new() + => options.AddSecurityDefinition(name, new OpenApiSecurityScheme() { In = location, Name = parameterName, @@ -195,7 +201,7 @@ public static void AddOAuth2Authentication(this SwaggerGenOptions options, strin ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(authFlow); - options.AddSecurityDefinition(name, new() + options.AddSecurityDefinition(name, new OpenApiSecurityScheme() { Type = SecuritySchemeType.OAuth2, Flows = new() @@ -207,6 +213,9 @@ public static void AddOAuth2Authentication(this SwaggerGenOptions options, strin AddSecurityRequirement(options, name); } + private static void AddSecurityRequirement(SwaggerGenOptions options, OpenApiSecurityRequirement requirement) + => options.AddSecurityRequirement(_ => requirement); + private static void AddSecurityRequirement(SwaggerGenOptions options, string name) - => options.AddSecurityRequirement(OpenApiHelpers.CreateSecurityRequirement(name)); + => options.AddSecurityRequirement(document => OpenApiHelpers.CreateSecurityRequirement(name, document)); } diff --git a/src/SimpleAuthentication/OpenApi/AuthenticationDocumentTransformer.cs b/src/SimpleAuthentication/OpenApi/AuthenticationDocumentTransformer.cs index 7996f7e..96b8e87 100644 --- a/src/SimpleAuthentication/OpenApi/AuthenticationDocumentTransformer.cs +++ b/src/SimpleAuthentication/OpenApi/AuthenticationDocumentTransformer.cs @@ -1,4 +1,4 @@ -#if NET9_0_OR_GREATER +#if NET9_0 using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.OpenApi; @@ -106,4 +106,122 @@ private static void AddSecurityRequirement(OpenApiDocument document, OpenApiSecu } } +#elif NET10_0_OR_GREATER + +using System.Xml.Linq; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.Extensions.Configuration; +using Microsoft.Net.Http.Headers; +using Microsoft.OpenApi; +using SimpleAuthentication.ApiKey; +using SimpleAuthentication.BasicAuthentication; +using SimpleAuthentication.JwtBearer; + +namespace SimpleAuthentication.OpenApi; + +internal class AuthenticationDocumentTransformer(IConfiguration configuration, string sectionName, IEnumerable additionalSecurityRequirements, IEnumerable additionalSecurityDefinitionNames) : 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()) + { + // Adds all the other security requirements that have been specified. + foreach (var securityRequirement in additionalSecurityRequirements) + { + AddSecurityRequirement(document, securityRequirement); + } + } + + if (additionalSecurityDefinitionNames.Any()) + { + // Adds all the other security definitions that have been specified. + foreach (var definitionName in additionalSecurityDefinitionNames) + { + AddSecurityRequirement(document, definitionName); + } + } + + return Task.CompletedTask; + + static void CheckAddJwtBearer(OpenApiDocument document, IConfigurationSection section) + { + var settings = section.Get(); + 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(); + if (settings is null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(settings.HeaderName)) + { + var schemeName = $"{settings.SchemeName}-Header"; + + AddSecurityScheme(document, schemeName, SecuritySchemeType.ApiKey, null, ParameterLocation.Header, settings.HeaderName, "Insert the API Key"); + AddSecurityRequirement(document, schemeName); + } + + if (!string.IsNullOrWhiteSpace(settings.QueryStringKey)) + { + var schemeName = $"{settings.SchemeName}-QueryString"; + + AddSecurityScheme(document, schemeName, SecuritySchemeType.ApiKey, null, ParameterLocation.Query, settings.QueryStringKey, "Insert the API Key"); + AddSecurityRequirement(document, schemeName); + } + } + + static void CheckAddBasicAuthentication(OpenApiDocument document, IConfigurationSection section) + { + var settings = section.Get(); + 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(); + + document.Components.SecuritySchemes.Add(name, new OpenApiSecurityScheme() + { + In = location, + Name = parameterName, + Description = description, + Type = securitySchemeType, + Scheme = scheme + }); + } + + internal static void AddSecurityRequirement(OpenApiDocument document, string name) + => AddSecurityRequirement(document, OpenApiHelpers.CreateSecurityRequirement(name, document)); + + private static void AddSecurityRequirement(OpenApiDocument document, OpenApiSecurityRequirement requirement) + { + document.Security ??= []; + document.Security.Add(requirement); + } +} + #endif \ No newline at end of file diff --git a/src/SimpleAuthentication/OpenApi/AuthenticationOperationTransformer.cs b/src/SimpleAuthentication/OpenApi/AuthenticationOperationTransformer.cs index 4883f63..1e1f005 100644 --- a/src/SimpleAuthentication/OpenApi/AuthenticationOperationTransformer.cs +++ b/src/SimpleAuthentication/OpenApi/AuthenticationOperationTransformer.cs @@ -1,4 +1,4 @@ -#if NET9_0_OR_GREATER +#if NET9_0 using System.Net; using Microsoft.AspNetCore.Authorization; @@ -30,4 +30,37 @@ public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTra } } +#elif NET10_0_OR_GREATER + +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +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 ??= []; + operation.Responses.TryAdd(StatusCodes.Status401Unauthorized.ToString(), OpenApiHelpers.CreateResponse(HttpStatusCode.Unauthorized.ToString())); + operation.Responses.TryAdd(StatusCodes.Status403Forbidden.ToString(), OpenApiHelpers.CreateResponse(HttpStatusCode.Forbidden.ToString())); + } + } +} + #endif \ No newline at end of file diff --git a/src/SimpleAuthentication/OpenApi/DefaultResponseDocumentTransformer.cs b/src/SimpleAuthentication/OpenApi/DefaultResponseDocumentTransformer.cs index 3baaff0..78cb6d4 100644 --- a/src/SimpleAuthentication/OpenApi/DefaultResponseDocumentTransformer.cs +++ b/src/SimpleAuthentication/OpenApi/DefaultResponseDocumentTransformer.cs @@ -1,4 +1,4 @@ -#if NET9_0_OR_GREATER +#if NET9_0 using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OpenApi; @@ -60,4 +60,23 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC } } +#elif NET10_0_OR_GREATER + +using System.Net.Mime; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace SimpleAuthentication.OpenApi; + +internal class DefaultResponseDocumentTransformer : IOpenApiDocumentTransformer +{ + public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + // Generate schema for error responses. + var errorSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), cancellationToken: cancellationToken); + document.AddComponent(nameof(ProblemDetails), errorSchema); + } +} + #endif \ No newline at end of file diff --git a/src/SimpleAuthentication/OpenApi/OAuth2AuthenticationDocumentTransformer.cs b/src/SimpleAuthentication/OpenApi/OAuth2AuthenticationDocumentTransformer.cs index 8325a7e..3fb8b6e 100644 --- a/src/SimpleAuthentication/OpenApi/OAuth2AuthenticationDocumentTransformer.cs +++ b/src/SimpleAuthentication/OpenApi/OAuth2AuthenticationDocumentTransformer.cs @@ -1,4 +1,4 @@ -#if NET9_0_OR_GREATER +#if NET9_0 using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi.Models; @@ -27,4 +27,33 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC } } +#elif NET10_0_OR_GREATER + +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace SimpleAuthentication.OpenApi; + +internal class OAuth2AuthenticationDocumentTransformer(string name, OpenApiOAuthFlow authFlow) : IOpenApiDocumentTransformer +{ + public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + document.Components ??= new(); + document.Components.SecuritySchemes ??= new Dictionary(); + + document.Components.SecuritySchemes.Add(name, new OpenApiSecurityScheme() + { + Type = SecuritySchemeType.OAuth2, + Flows = new() + { + Implicit = authFlow + } + }); + + AuthenticationDocumentTransformer.AddSecurityRequirement(document, name); + + return Task.CompletedTask; + } +} + #endif \ No newline at end of file diff --git a/src/SimpleAuthentication/OpenApi/OpenApiHelpers.cs b/src/SimpleAuthentication/OpenApi/OpenApiHelpers.cs index 737b882..9e3cb31 100644 --- a/src/SimpleAuthentication/OpenApi/OpenApiHelpers.cs +++ b/src/SimpleAuthentication/OpenApi/OpenApiHelpers.cs @@ -1,4 +1,4 @@ -#if NET9_0_OR_GREATER +#if NET9_0 using System.Net.Mime; using Microsoft.AspNetCore.Mvc; @@ -45,4 +45,41 @@ public static OpenApiResponse CreateResponse(string description) }; } +#elif NET10_0_OR_GREATER + +using System.Net.Mime; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OpenApi; + +namespace SimpleAuthentication.OpenApi; + +internal static class OpenApiHelpers +{ + public static OpenApiSecurityRequirement CreateSecurityRequirement(string name, OpenApiDocument document) + => new() + { + { new OpenApiSecuritySchemeReference(name, document), [] } + }; + + public static OpenApiResponse CreateResponse(string description) + => new() + { + Description = description, + Content = new Dictionary + { + [MediaTypeNames.Application.ProblemJson] = new() + { + Schema = new OpenApiSchemaReference(nameof(ProblemDetails)) + { + Reference = new() + { + Type = ReferenceType.Schema, + Id = nameof(ProblemDetails) + } + } + } + } + }; +} + #endif \ No newline at end of file diff --git a/src/SimpleAuthentication/OpenApiExtensions.cs b/src/SimpleAuthentication/OpenApiExtensions.cs index 4837d46..1abd253 100644 --- a/src/SimpleAuthentication/OpenApiExtensions.cs +++ b/src/SimpleAuthentication/OpenApiExtensions.cs @@ -1,4 +1,4 @@ -#if NET9_0_OR_GREATER +#if NET9_0 using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.Configuration; @@ -122,4 +122,126 @@ public static void AddOAuth2Authentication(this OpenApiOptions options, string n } } +#elif NET10_0_OR_GREATER + +using Microsoft.AspNetCore.OpenApi; +using Microsoft.Extensions.Configuration; +using Microsoft.OpenApi; +using SimpleAuthentication.OpenApi; + +namespace SimpleAuthentication; + +/// +/// Provides extension methods for adding authentication support in OpenAPI. +/// +public static class OpenApiExtensions +{ + /// + /// Adds authentication support in OpenAPI, reading configuration from the specified source. + /// + /// The to add configuration to. + /// The being bound. + /// The name of the configuration section that holds authentication settings (default: Authentication). + /// + /// + public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, string sectionName = "Authentication") + => options.AddSimpleAuthentication(configuration, sectionName, [], []); + + /// + /// Adds authentication support in OpenAPI, reading configuration from a section named Authentication in source. + /// + /// The to add configuration to. + /// The being bound. + /// The name of additional security definitions that have been defined in OpenAPI. + /// + /// + public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, params IEnumerable additionalSecurityDefinitionNames) + => options.AddSimpleAuthentication(configuration, "Authentication", additionalSecurityDefinitionNames); + + /// + /// Adds authentication support in OpenAPI, reading configuration from the specified source. + /// + /// The to add configuration to. + /// The being bound. + /// The name of the configuration section that holds authentication settings (default: Authentication). + /// The name of additional security definitions that have been defined in OpenAPI. + /// + /// + public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, string sectionName, params IEnumerable additionalSecurityDefinitionNames) + => options.AddSimpleAuthentication(configuration, sectionName, [], additionalSecurityDefinitionNames); + + /// + /// Adds authentication support in OpenAPI, reading configuration from the specified source. + /// + /// The to add configuration to. + /// The being bound. + /// Additional security requirements to be added to OpenAPI definition. + /// + /// + public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, params IEnumerable securityRequirements) + => options.AddSimpleAuthentication(configuration, "Authentication", securityRequirements, []); + + /// + /// Adds authentication support in OpenAPI, reading configuration from the specified source. + /// + /// The to add configuration to. + /// The being bound. + /// The name of the configuration section that holds authentication settings (default: Authentication). + /// Additional security requirements to be added to OpenAPI definition. + /// The name of additional security definitions that have been defined in OpenAPI. + /// + /// + public static void AddSimpleAuthentication(this OpenApiOptions options, IConfiguration configuration, string sectionName, IEnumerable additionalSecurityRequirements, IEnumerable additionalSecurityDefinitionNames) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); + + options.AddDocumentTransformer(new AuthenticationDocumentTransformer(configuration, sectionName, additionalSecurityRequirements, additionalSecurityDefinitionNames)); + options.AddDocumentTransformer(); + options.AddOperationTransformer(); + } + + /// + /// Adds OAuth2 authentication support in OpenAPI, for example to integrate with Microsoft.Identity.Web. + /// + /// The to add configuration to. + /// The name of the OAuth2 authentication scheme to add. + /// The Authorization Url for OAuth2 authorization. + /// The Token Url for OAuth2 authorization. + /// The list of scopes. + /// + public static void AddOAuth2Authentication(this OpenApiOptions options, string name, string authorizationUrl, string tokenUrl, IDictionary scopes) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentException.ThrowIfNullOrWhiteSpace(authorizationUrl); + ArgumentException.ThrowIfNullOrWhiteSpace(tokenUrl); + ArgumentNullException.ThrowIfNull(scopes); + + options.AddOAuth2Authentication(name, new() + { + AuthorizationUrl = new(authorizationUrl), + TokenUrl = new(tokenUrl), + Scopes = scopes + }); + } + + /// + /// Adds OAuth2 authentication support in OpenAPI, for example to integrate with Microsoft.Identity.Web. + /// + /// The to add configuration to. + /// The name of the OAuth2 authentication scheme to add. + /// The object that describes the authorization flow. + /// + /// + public static void AddOAuth2Authentication(this OpenApiOptions options, string name, OpenApiOAuthFlow authFlow) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(authFlow); + + options.AddDocumentTransformer(new OAuth2AuthenticationDocumentTransformer(name, authFlow)); + } +} + #endif \ No newline at end of file diff --git a/src/SimpleAuthentication/SimpleAuthentication.csproj b/src/SimpleAuthentication/SimpleAuthentication.csproj index 14d2622..d7457d8 100644 --- a/src/SimpleAuthentication/SimpleAuthentication.csproj +++ b/src/SimpleAuthentication/SimpleAuthentication.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable latest @@ -31,11 +31,11 @@ - + - - + + @@ -46,4 +46,8 @@ + + + +