From 0dd9a75dd85bb8f59f9fe0d4b5ebf0d6cb998817 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 14 Jul 2026 14:07:07 -0400 Subject: [PATCH 1/2] fix: validate required properties of security scheme before serialization Signed-off-by: Vincent Biret --- .../Models/OpenApiSecurityScheme.cs | 21 +++ .../Models/OpenApiSecuritySchemeTests.cs | 155 ++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs index 626f7f2bd..35f491e5b 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs @@ -97,6 +97,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version Action callback) { Utils.CheckArgumentNull(writer); + EnsureRequiredPropertiesAreSet(version); writer.WriteStartObject(); @@ -176,6 +177,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version public virtual void SerializeAsV2(IOpenApiWriter writer) { Utils.CheckArgumentNull(writer); + EnsureRequiredPropertiesAreSet(OpenApiSpecVersion.OpenApi2_0); if (Type == SecuritySchemeType.Http && Scheme != OpenApiConstants.Basic) { @@ -239,6 +241,25 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) writer.WriteEndObject(); } + private void EnsureRequiredPropertiesAreSet(OpenApiSpecVersion version) + { + switch (Type) + { + case null: + throw new OpenApiException("The 'type' property is required for security schemes."); + case SecuritySchemeType.ApiKey when string.IsNullOrEmpty(Name): + throw new OpenApiException("The 'name' property is required for apiKey security schemes."); + case SecuritySchemeType.ApiKey when In is null: + throw new OpenApiException("The 'in' property is required for apiKey security schemes."); + case SecuritySchemeType.Http when version >= OpenApiSpecVersion.OpenApi3_0 && string.IsNullOrEmpty(Scheme): + throw new OpenApiException("The 'scheme' property is required for http security schemes."); + case SecuritySchemeType.OAuth2 when Flows is null: + throw new OpenApiException("The 'flows' property is required for oauth2 security schemes."); + case SecuritySchemeType.OpenIdConnect when version >= OpenApiSpecVersion.OpenApi3_0 && OpenIdConnectUrl is null: + throw new OpenApiException("The 'openIdConnectUrl' property is required for openIdConnect security schemes."); + } + } + /// /// Arbitrarily chooses one object from the /// to populate in V2 security scheme. diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs index b826c89b3..335d2d9b2 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs @@ -187,6 +187,155 @@ public async Task SerializeApiKeySecuritySchemeAsV3YamlWorks() Assert.Equal(expected, actual); } + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeSecuritySchemeWithoutTypeThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme(); + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'type' property is required for security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeApiKeySecuritySchemeWithoutNameThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Query + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'name' property is required for apiKey security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeApiKeySecuritySchemeWithoutInThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Name = "parameterName", + Type = SecuritySchemeType.ApiKey + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'in' property is required for apiKey security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeHttpSecuritySchemeWithoutSchemeThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'scheme' property is required for http security schemes."); + } + + [Fact] + public async Task SerializeHttpSecuritySchemeWithoutSchemeAsV2DoesNotThrow() + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http + }; + + // Act + var actual = await securityScheme.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi2_0); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse("{}"), JsonNode.Parse(actual))); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeOAuth2SecuritySchemeWithoutFlowsThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2 + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'flows' property is required for oauth2 security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeOpenIdConnectSecuritySchemeWithoutOpenIdConnectUrlThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OpenIdConnect + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'openIdConnectUrl' property is required for openIdConnect security schemes."); + } + + [Fact] + public async Task SerializeOpenIdConnectSecuritySchemeWithoutOpenIdConnectUrlAsV2DoesNotThrow() + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OpenIdConnect + }; + + // Act + var actual = await securityScheme.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi2_0); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse("{}"), JsonNode.Parse(actual))); + } + [Fact] public async Task SerializeHttpBasicSecuritySchemeAsV3JsonWorks() { @@ -498,5 +647,11 @@ public async Task SerializeDeprecatedSecuritySchemeAsV3JsonWorks() // Assert Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); } + + private static async Task AssertRequiredPropertyThrowsAsync(OpenApiSecurityScheme securityScheme, OpenApiSpecVersion version, string message) + { + var exception = await Assert.ThrowsAsync(() => securityScheme.SerializeAsJsonAsync(version)); + Assert.Contains(message, exception.Message); + } } } From 16826b07e4ab23b50d896dcebc3b8cc56dca33df Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 14 Jul 2026 14:21:35 -0400 Subject: [PATCH 2/2] chore: fixes mock setup Signed-off-by: Vincent Biret --- .../Mocks/OpenApiComponentsSerializationTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs b/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs index 144f28500..65b512b21 100644 --- a/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs @@ -26,6 +26,8 @@ public OpenApiComponentsSerializationTests() _components.Responses["200"] = _responseMock.Object; _components.Parameters["limit"] = _parameterMock.Object; _components.Headers["x-rate-limit"] = _headerMock.Object; + _securitySchemeMock.Object.Type = SecuritySchemeType.Http; + _securitySchemeMock.Object.Scheme = "bearer"; _components.SecuritySchemes["api_key"] = _securitySchemeMock.Object; _components.Links["UserRepositories"] = _linkMock.Object; _components.Callbacks["onData"] = _callbackMock.Object;