From 335cee5dece87d0be2060e0e5d494189734f9ba2 Mon Sep 17 00:00:00 2001
From: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Thu, 22 Jan 2026 09:13:17 -0800
Subject: [PATCH 1/2] feat(securityscheme): add oauth2 metadata url support
---
.../Interfaces/IOpenApiSecurityScheme.cs | 6 ++
.../Models/OpenApiConstants.cs | 5 ++
.../Models/OpenApiSecurityScheme.cs | 9 +++
.../OpenApiSecuritySchemeReference.cs | 3 +
.../V32/OpenApiSecuritySchemeDeserializer.cs | 12 ++-
.../V32Tests/OpenApiSecuritySchemeTests.cs | 30 ++++++++
.../oauth2SecuritySchemeWithMetadataUrl.yaml | 8 ++
.../Models/OpenApiSecuritySchemeTests.cs | 73 +++++++++++++++++++
8 files changed, 145 insertions(+), 1 deletion(-)
create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/oauth2SecuritySchemeWithMetadataUrl.yaml
diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSecurityScheme.cs
index 41247ce08..16da4a881 100644
--- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSecurityScheme.cs
+++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSecurityScheme.cs
@@ -46,6 +46,12 @@ public interface IOpenApiSecurityScheme : IOpenApiDescribedElement, IOpenApiRead
///
public Uri? OpenIdConnectUrl { get; }
+ ///
+ /// URL to the OAuth2 Authorization Server Metadata document (RFC 8414).
+ /// Note: This field is supported in OpenAPI 3.2.0+ only.
+ ///
+ public Uri? OAuth2MetadataUrl { get; }
+
///
/// Specifies that a security scheme is deprecated and SHOULD be transitioned out of usage.
/// Note: This field is supported in OpenAPI 3.2.0+. For earlier versions, it will be serialized as x-oai-deprecated extension.
diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
index b87c9079d..543baae29 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
@@ -695,6 +695,11 @@ public static class OpenApiConstants
///
public const string Flows = "flows";
+ ///
+ /// Field: Oauth2MetadataUrl
+ ///
+ public const string OAuth2MetadataUrl = "oauth2MetadataUrl";
+
///
/// Field: OpenIdConnectUrl
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
index 57603e0aa..6278096b2 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
@@ -35,6 +35,9 @@ public class OpenApiSecurityScheme : IOpenApiExtensible, IOpenApiSecurityScheme
///
public Uri? OpenIdConnectUrl { get; set; }
+ ///
+ public Uri? OAuth2MetadataUrl { get; set; }
+
///
public bool Deprecated { get; set; }
@@ -60,6 +63,7 @@ internal OpenApiSecurityScheme(IOpenApiSecurityScheme securityScheme)
BearerFormat = securityScheme.BearerFormat ?? BearerFormat;
Flows = securityScheme.Flows != null ? new(securityScheme.Flows) : null;
OpenIdConnectUrl = securityScheme.OpenIdConnectUrl != null ? new Uri(securityScheme.OpenIdConnectUrl.OriginalString, UriKind.RelativeOrAbsolute) : null;
+ OAuth2MetadataUrl = securityScheme.OAuth2MetadataUrl != null ? new Uri(securityScheme.OAuth2MetadataUrl.OriginalString, UriKind.RelativeOrAbsolute) : null;
Deprecated = securityScheme.Deprecated;
Extensions = securityScheme.Extensions != null ? new Dictionary(securityScheme.Extensions) : null;
}
@@ -118,7 +122,12 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
break;
case SecuritySchemeType.OAuth2:
// This property apply to oauth2 type only.
+ // oauth2MetadataUrl
// flows
+ if (version >= OpenApiSpecVersion.OpenApi3_2)
+ {
+ writer.WriteProperty(OpenApiConstants.OAuth2MetadataUrl, OAuth2MetadataUrl?.ToString());
+ }
writer.WriteOptionalObject(OpenApiConstants.Flows, Flows, callback);
break;
case SecuritySchemeType.OpenIdConnect:
diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs
index 4267315e3..25ed6e25f 100644
--- a/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs
+++ b/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs
@@ -54,6 +54,9 @@ public string? Description
///
public Uri? OpenIdConnectUrl { get => Target?.OpenIdConnectUrl; }
+ ///
+ public Uri? OAuth2MetadataUrl { get => Target?.OAuth2MetadataUrl; }
+
///
public IDictionary? Extensions { get => Target?.Extensions; }
diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs
index 34e3f589e..f0acc7ea6 100644
--- a/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
@@ -68,6 +68,16 @@ internal static partial class OpenApiV32Deserializer
}
}
},
+ {
+ "oauth2MetadataUrl", (o, n, _) =>
+ {
+ var metadataUrl = n.GetScalarValue();
+ if (metadataUrl != null)
+ {
+ o.OAuth2MetadataUrl = new(metadataUrl, UriKind.RelativeOrAbsolute);
+ }
+ }
+ },
{
"flows", (o, n, t) =>
{
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs
index ea0937237..50c836f66 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs
@@ -86,6 +86,36 @@ public async Task ParseOAuth2SecuritySchemeShouldSucceed()
}, securityScheme);
}
+ [Fact]
+ public async Task ParseOAuth2SecuritySchemeWithMetadataUrlShouldSucceed()
+ {
+ // Act
+ var securityScheme = await OpenApiModelFactory.LoadAsync(
+ Path.Combine(SampleFolderPath, "oauth2SecuritySchemeWithMetadataUrl.yaml"),
+ OpenApiSpecVersion.OpenApi3_2,
+ new(),
+ SettingsFixture.ReaderSettings);
+
+ // Assert
+ Assert.Equivalent(
+ new OpenApiSecurityScheme
+ {
+ Type = SecuritySchemeType.OAuth2,
+ OAuth2MetadataUrl = new Uri("https://idp.example.com/.well-known/oauth-authorization-server"),
+ Flows = new OpenApiOAuthFlows
+ {
+ ClientCredentials = new OpenApiOAuthFlow
+ {
+ TokenUrl = new Uri("https://idp.example.com/oauth/token"),
+ Scopes = new System.Collections.Generic.Dictionary
+ {
+ ["scope:one"] = "Scope one"
+ }
+ }
+ }
+ }, securityScheme);
+ }
+
[Fact]
public async Task ParseOpenIdConnectSecuritySchemeShouldSucceed()
{
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/oauth2SecuritySchemeWithMetadataUrl.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/oauth2SecuritySchemeWithMetadataUrl.yaml
new file mode 100644
index 000000000..612f65fde
--- /dev/null
+++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/oauth2SecuritySchemeWithMetadataUrl.yaml
@@ -0,0 +1,8 @@
+# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.2.0.md#securitySchemeObject
+type: oauth2
+oauth2MetadataUrl: https://idp.example.com/.well-known/oauth-authorization-server
+flows:
+ clientCredentials:
+ tokenUrl: https://idp.example.com/oauth/token
+ scopes:
+ scope:one: Scope one
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
index cd8499b9f..6a234c99f 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
@@ -93,6 +93,24 @@ public class OpenApiSecuritySchemeTests
}
};
+ private static OpenApiSecurityScheme OAuth2MetadataSecurityScheme => new()
+ {
+ Description = "description1",
+ Type = SecuritySchemeType.OAuth2,
+ OAuth2MetadataUrl = new("https://idp.example.com/.well-known/oauth-authorization-server"),
+ Flows = new()
+ {
+ ClientCredentials = new()
+ {
+ TokenUrl = new("https://idp.example.com/oauth/token"),
+ Scopes = new Dictionary
+ {
+ ["scope:one"] = "Scope one"
+ }
+ }
+ }
+ };
+
private static OpenApiSecurityScheme OpenIdConnectSecurityScheme => new()
{
Description = "description1",
@@ -257,6 +275,61 @@ public async Task SerializeOAuthSingleFlowSecuritySchemeAsV3JsonWorks()
Assert.Equal(expected, actual);
}
+ [Fact]
+ public async Task SerializeOAuthSecuritySchemeWithMetadataUrlAsV32JsonWorks()
+ {
+ // Arrange
+ var expected =
+ """
+ {
+ "type": "oauth2",
+ "description": "description1",
+ "oauth2MetadataUrl": "https://idp.example.com/.well-known/oauth-authorization-server",
+ "flows": {
+ "clientCredentials": {
+ "tokenUrl": "https://idp.example.com/oauth/token",
+ "scopes": {
+ "scope:one": "Scope one"
+ }
+ }
+ }
+ }
+ """;
+
+ // Act
+ var actual = await OAuth2MetadataSecurityScheme.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2);
+
+ // Assert
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
+ }
+
+ [Fact]
+ public async Task SerializeOAuthSecuritySchemeWithMetadataUrlAsV31JsonOmitsMetadataUrl()
+ {
+ // Arrange
+ var expected =
+ """
+ {
+ "type": "oauth2",
+ "description": "description1",
+ "flows": {
+ "clientCredentials": {
+ "tokenUrl": "https://idp.example.com/oauth/token",
+ "scopes": {
+ "scope:one": "Scope one"
+ }
+ }
+ }
+ }
+ """;
+
+ // Act
+ var actual = await OAuth2MetadataSecurityScheme.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);
+
+ // Assert
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
+ }
+
[Fact]
public async Task SerializeOAuthMultipleFlowSecuritySchemeAsV3JsonWorks()
{
From 14b73bf3288eb56a12eb53e401422561b01f87d4 Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Thu, 22 Jan 2026 11:25:14 -0800
Subject: [PATCH 2/2] Add x-oauth2-metadata-url serialization for OAuth2
schemes
When serializing OAuth2 security schemes, the OAuth2MetadataUrl is now written as 'x-oauth2-metadata-url' for OpenAPI 3.1 and later. Updated tests and public API documentation to reflect this change.
---
src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs | 4 ++++
.../Models/OpenApiSecuritySchemeTests.cs | 1 +
test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt | 4 ++++
3 files changed, 9 insertions(+)
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
index 6278096b2..b1670de6d 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
@@ -128,6 +128,10 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
{
writer.WriteProperty(OpenApiConstants.OAuth2MetadataUrl, OAuth2MetadataUrl?.ToString());
}
+ else
+ {
+ writer.WriteProperty("x-oauth2-metadata-url", OAuth2MetadataUrl?.ToString());
+ }
writer.WriteOptionalObject(OpenApiConstants.Flows, Flows, callback);
break;
case SecuritySchemeType.OpenIdConnect:
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
index 6a234c99f..b826c89b3 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
@@ -312,6 +312,7 @@ public async Task SerializeOAuthSecuritySchemeWithMetadataUrlAsV31JsonOmitsMetad
{
"type": "oauth2",
"description": "description1",
+ "x-oauth2-metadata-url": "https://idp.example.com/.well-known/oauth-authorization-server",
"flows": {
"clientCredentials": {
"tokenUrl": "https://idp.example.com/oauth/token",
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index 5413879c0..b1c9810e5 100644
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -288,6 +288,7 @@ namespace Microsoft.OpenApi
Microsoft.OpenApi.OpenApiOAuthFlows? Flows { get; }
Microsoft.OpenApi.ParameterLocation? In { get; }
string? Name { get; }
+ System.Uri? OAuth2MetadataUrl { get; }
System.Uri? OpenIdConnectUrl { get; }
string? Scheme { get; }
Microsoft.OpenApi.SecuritySchemeType? Type { get; }
@@ -538,6 +539,7 @@ namespace Microsoft.OpenApi
public const string Null = "null";
public const string Nullable = "nullable";
public const string NullableExtension = "x-nullable";
+ public const string OAuth2MetadataUrl = "oauth2MetadataUrl";
public const string OneOf = "oneOf";
public const string OpenApi = "openapi";
public const string OpenIdConnectUrl = "openIdConnectUrl";
@@ -1381,6 +1383,7 @@ namespace Microsoft.OpenApi
public Microsoft.OpenApi.OpenApiOAuthFlows? Flows { get; set; }
public Microsoft.OpenApi.ParameterLocation? In { get; set; }
public string? Name { get; set; }
+ public System.Uri? OAuth2MetadataUrl { get; set; }
public System.Uri? OpenIdConnectUrl { get; set; }
public string? Scheme { get; set; }
public Microsoft.OpenApi.SecuritySchemeType? Type { get; set; }
@@ -1400,6 +1403,7 @@ namespace Microsoft.OpenApi
public Microsoft.OpenApi.OpenApiOAuthFlows? Flows { get; }
public Microsoft.OpenApi.ParameterLocation? In { get; }
public string? Name { get; }
+ public System.Uri? OAuth2MetadataUrl { get; }
public System.Uri? OpenIdConnectUrl { get; }
public string? Scheme { get; }
public Microsoft.OpenApi.SecuritySchemeType? Type { get; }