Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,11 @@ public static class OpenApiConstants
/// </summary>
public const string AuthorizationCode = "authorizationCode";

/// <summary>
/// Field: DeviceAuthorization
/// </summary>
public const string DeviceAuthorization = "deviceAuthorization";

/// <summary>
/// Field: AuthorizationUrl
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiOAuthFlows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public class OpenApiOAuthFlows : IOpenApiSerializable, IOpenApiExtensible
/// </summary>
public OpenApiOAuthFlow? AuthorizationCode { get; set; }

/// <summary>
/// Configuration for the OAuth Device Authorization flow.
/// </summary>
public OpenApiOAuthFlow? DeviceAuthorization { get; set; }

/// <summary>
/// Specification Extensions.
/// </summary>
Expand All @@ -51,6 +56,7 @@ public OpenApiOAuthFlows(OpenApiOAuthFlows oAuthFlows)
Password = oAuthFlows?.Password != null ? new(oAuthFlows.Password) : null;
ClientCredentials = oAuthFlows?.ClientCredentials != null ? new(oAuthFlows.ClientCredentials) : null;
AuthorizationCode = oAuthFlows?.AuthorizationCode != null ? new(oAuthFlows.AuthorizationCode) : null;
DeviceAuthorization = oAuthFlows?.DeviceAuthorization != null ? new(oAuthFlows.DeviceAuthorization) : null;
Extensions = oAuthFlows?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(oAuthFlows.Extensions) : null;
}

Expand Down Expand Up @@ -106,6 +112,22 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
AuthorizationCode,
callback);

// deviceAuthorization - only for v3.2+, otherwise as extension
if (version >= OpenApiSpecVersion.OpenApi3_2)
{
writer.WriteOptionalObject(
OpenApiConstants.DeviceAuthorization,
DeviceAuthorization,
callback);
}
else if (DeviceAuthorization is not null)
{
writer.WriteOptionalObject(
OpenApiConstants.ExtensionFieldNamePrefix + "oai-" + OpenApiConstants.DeviceAuthorization,
DeviceAuthorization,
callback);
}

// extensions
writer.WriteExtensions(Extensions, version);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal static partial class OpenApiV3Deserializer
private static readonly PatternFieldMap<OpenApiOAuthFlows> _oAuthFlowsPatternFields =
new()
{
{s => s.Equals("x-oai-deviceAuthorization", StringComparison.OrdinalIgnoreCase), (o, p, n, t) => o.DeviceAuthorization = LoadOAuthFlow(n, t)},
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal static partial class OpenApiV31Deserializer
private static readonly PatternFieldMap<OpenApiOAuthFlows> _oAuthFlowsPatternFields =
new()
{
{s => s.Equals("x-oai-deviceAuthorization", StringComparison.OrdinalIgnoreCase), (o, p, n, t) => o.DeviceAuthorization = LoadOAuthFlow(n, t)},
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ internal static partial class OpenApiV32Deserializer
{"implicit", (o, n, t) => o.Implicit = LoadOAuthFlow(n, t)},
{"password", (o, n, t) => o.Password = LoadOAuthFlow(n, t)},
{"clientCredentials", (o, n, t) => o.ClientCredentials = LoadOAuthFlow(n, t)},
{"authorizationCode", (o, n, t) => o.AuthorizationCode = LoadOAuthFlow(n, t)}
{"authorizationCode", (o, n, t) => o.AuthorizationCode = LoadOAuthFlow(n, t)},
{"deviceAuthorization", (o, n, t) => o.DeviceAuthorization = LoadOAuthFlow(n, t)}
};

private static readonly PatternFieldMap<OpenApiOAuthFlows> _oAuthFlowsPatternFields =
Expand Down
90 changes: 90 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using System.Collections.Generic;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -48,6 +49,20 @@ public class OpenApiOAuthFlowsTests
}
};

public static OpenApiOAuthFlows OAuthFlowsWithDeviceAuthorization = new()
{
DeviceAuthorization = new()
{
TokenUrl = new("http://example.com/token"),
RefreshUrl = new("http://example.com/refresh"),
Scopes = new Dictionary<string, string>
{
["scopeName1"] = "description1",
["scopeName2"] = "description2"
}
}
};

[Fact]
public async Task SerializeBasicOAuthFlowsAsV3JsonWorks()
{
Expand Down Expand Up @@ -139,5 +154,80 @@ public async Task SerializeOAuthFlowsWithMultipleFlowsAsV3JsonWorks()
expected = expected.MakeLineBreaksEnvironmentNeutral();
Assert.Equal(expected, actual);
}

[Fact]
public async Task SerializeOAuthFlowsWithDeviceAuthorizationAsV32JsonWorks()
{
// Arrange
var expected =
"""
{
"deviceAuthorization": {
"tokenUrl": "http://example.com/token",
"refreshUrl": "http://example.com/refresh",
"scopes": {
"scopeName1": "description1",
"scopeName2": "description2"
}
}
}
""";

// Act
var actual = await OAuthFlowsWithDeviceAuthorization.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2);

// Assert
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Fact]
public async Task SerializeOAuthFlowsWithDeviceAuthorizationAsV31JsonWorks()
{
// Arrange
var expected =
"""
{
"x-oai-deviceAuthorization": {
"tokenUrl": "http://example.com/token",
"refreshUrl": "http://example.com/refresh",
"scopes": {
"scopeName1": "description1",
"scopeName2": "description2"
}
}
}
""";

// Act
var actual = await OAuthFlowsWithDeviceAuthorization.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);

// Assert
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Fact]
public async Task SerializeOAuthFlowsWithDeviceAuthorizationAsV3JsonWorks()
{
// Arrange
var expected =
"""
{
"x-oai-deviceAuthorization": {
"tokenUrl": "http://example.com/token",
"refreshUrl": "http://example.com/refresh",
"scopes": {
"scopeName1": "description1",
"scopeName2": "description2"
}
}
}
""";

// Act
var actual = await OAuthFlowsWithDeviceAuthorization.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);

// Assert
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}
}
}
2 changes: 2 additions & 0 deletions test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ namespace Microsoft.OpenApi
public const string DependentRequired = "dependentRequired";
public const string Deprecated = "deprecated";
public const string Description = "description";
public const string DeviceAuthorization = "deviceAuthorization";
public const string Discriminator = "discriminator";
public const string DollarRef = "$ref";
public const string DollarSchema = "$schema";
Expand Down Expand Up @@ -927,6 +928,7 @@ namespace Microsoft.OpenApi
public OpenApiOAuthFlows(Microsoft.OpenApi.OpenApiOAuthFlows oAuthFlows) { }
public Microsoft.OpenApi.OpenApiOAuthFlow? AuthorizationCode { get; set; }
public Microsoft.OpenApi.OpenApiOAuthFlow? ClientCredentials { get; set; }
public Microsoft.OpenApi.OpenApiOAuthFlow? DeviceAuthorization { get; set; }
public System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.IOpenApiExtension>? Extensions { get; set; }
public Microsoft.OpenApi.OpenApiOAuthFlow? Implicit { get; set; }
public Microsoft.OpenApi.OpenApiOAuthFlow? Password { get; set; }
Expand Down
Loading