Skip to content

Commit

Permalink
Swagger UI refactoring (domaindrivendev#2942)
Browse files Browse the repository at this point in the history
- Add explicit `[JsonPropertyName]` attributes to the Swagger UI configuration objects.
- Apply refactoring suggestions.
- Use HTTPS with Kestrel.
- Fix broken operation filter.
- Update old URLs.
  • Loading branch information
martincostello authored Jul 1, 2024
1 parent 7f7f46f commit 65a8035
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static IApplicationBuilder UseSwaggerUI(
if (options.ConfigObject.Urls == null)
{
var hostingEnv = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
options.ConfigObject.Urls = new[] { new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" } };
options.ConfigObject.Urls = [new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" }];
}

return app.UseSwaggerUI(options);
Expand Down
27 changes: 27 additions & 0 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,30 +72,37 @@ public class ConfigObject
/// <summary>
/// One or more Swagger JSON endpoints (url and name) to power the UI
/// </summary>
[JsonPropertyName("urls")]
public IEnumerable<UrlDescriptor> Urls { get; set; } = null;

/// <summary>
/// If set to true, enables deep linking for tags and operations
/// </summary>
[JsonPropertyName("deepLinking")]
public bool DeepLinking { get; set; } = false;

/// <summary>
/// If set to true, it persists authorization data and it would not be lost on browser close/refresh
/// </summary>
[JsonPropertyName("persistAuthorization")]
public bool PersistAuthorization { get; set; } = false;

/// <summary>
/// Controls the display of operationId in operations list
/// </summary>
[JsonPropertyName("displayOperationId")]
public bool DisplayOperationId { get; set; } = false;

/// <summary>
/// The default expansion depth for models (set to -1 completely hide the models)
/// </summary>
[JsonPropertyName("defaultModelsExpandDepth")]
public int DefaultModelsExpandDepth { get; set; } = 1;

/// <summary>
/// The default expansion depth for the model on the model-example section
/// </summary>
[JsonPropertyName("defaultModelExpandDepth")]
public int DefaultModelExpandDepth { get; set; } = 1;

/// <summary>
Expand All @@ -105,11 +112,13 @@ public class ConfigObject
#if NET6_0_OR_GREATER
[JsonConverter(typeof(JavascriptStringEnumConverter<ModelRendering>))]
#endif
[JsonPropertyName("defaultModelRendering")]
public ModelRendering DefaultModelRendering { get; set; } = ModelRendering.Example;

/// <summary>
/// Controls the display of the request duration (in milliseconds) for Try-It-Out requests
/// </summary>
[JsonPropertyName("displayRequestDuration")]
public bool DisplayRequestDuration { get; set; } = false;

/// <summary>
Expand All @@ -119,28 +128,33 @@ public class ConfigObject
#if NET6_0_OR_GREATER
[JsonConverter(typeof(JavascriptStringEnumConverter<DocExpansion>))]
#endif
[JsonPropertyName("docExpansion")]
public DocExpansion DocExpansion { get; set; } = DocExpansion.List;

/// <summary>
/// If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations
/// that are shown. Can be an empty string or specific value, in which case filtering will be enabled using that
/// value as the filter expression. Filtering is case sensitive matching the filter expression anywhere inside the tag
/// </summary>
[JsonPropertyName("filter")]
public string Filter { get; set; } = null;

/// <summary>
/// If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations
/// </summary>
[JsonPropertyName("maxDisplayedTags")]
public int? MaxDisplayedTags { get; set; } = null;

/// <summary>
/// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema
/// </summary>
[JsonPropertyName("showExtensions")]
public bool ShowExtensions { get; set; } = false;

/// <summary>
/// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters
/// </summary>
[JsonPropertyName("showCommonExtensions")]
public bool ShowCommonExtensions { get; set; } = false;

/// <summary>
Expand All @@ -156,6 +170,7 @@ public class ConfigObject
#if NET6_0_OR_GREATER
[JsonConverter(typeof(JavascriptStringEnumEnumerableConverter<SubmitMethod>))]
#endif
[JsonPropertyName("supportedSubmitMethods")]
public IEnumerable<SubmitMethod> SupportedSubmitMethods { get; set; } =
#if NET5_0_OR_GREATER
Enum.GetValues<SubmitMethod>();
Expand All @@ -174,6 +189,7 @@ public class ConfigObject
/// You can use this parameter to set a different validator URL, for example for locally deployed validators (Validator Badge).
/// Setting it to null will disable validation
/// </summary>
[JsonPropertyName("validatorUrl")]
public string ValidatorUrl { get; set; } = null;

[JsonExtensionData]
Expand All @@ -182,8 +198,10 @@ public class ConfigObject

public class UrlDescriptor
{
[JsonPropertyName("url")]
public string Url { get; set; }

[JsonPropertyName("name")]
public string Name { get; set; }
}

Expand Down Expand Up @@ -222,50 +240,59 @@ public class OAuthConfigObject
/// <summary>
/// Default clientId
/// </summary>
[JsonPropertyName("clientId")]
public string ClientId { get; set; } = null;

/// <summary>
/// Default clientSecret
/// </summary>
/// <remarks>Setting this exposes the client secrets in inline javascript in the swagger-ui generated html.</remarks>
[JsonPropertyName("clientSecret")]
public string ClientSecret { get; set; } = null;

/// <summary>
/// Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl
/// </summary>
[JsonPropertyName("realm")]
public string Realm { get; set; } = null;

/// <summary>
/// Application name, displayed in authorization popup
/// </summary>
[JsonPropertyName("appName")]
public string AppName { get; set; } = null;

/// <summary>
/// Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20)
/// </summary>
[JsonPropertyName("scopeSeparator")]
public string ScopeSeparator { get; set; } = " ";

/// <summary>
/// String array of initially selected oauth scopes, default is empty array
/// </summary>
[JsonPropertyName("scopes")]
public IEnumerable<string> Scopes { get; set; } = [];

/// <summary>
/// Additional query parameters added to authorizationUrl and tokenUrl
/// </summary>
[JsonPropertyName("additionalQueryStringParams")]
public Dictionary<string, string> AdditionalQueryStringParams { get; set; } = null;

/// <summary>
/// Only activated for the accessCode flow. During the authorization_code request to the tokenUrl,
/// pass the Client Password using the HTTP Basic Authentication scheme
/// (Authorization header with Basic base64encode(client_id + client_secret))
/// </summary>
[JsonPropertyName("useBasicAuthenticationWithAccessCodeGrant")]
public bool UseBasicAuthenticationWithAccessCodeGrant { get; set; } = false;

/// <summary>
/// Only applies to authorizatonCode flows. Proof Key for Code Exchange brings enhanced security for OAuth public clients.
/// The default is false
/// </summary>
[JsonPropertyName("usePkceWithAuthorizationCodeGrant")]
public bool UsePkceWithAuthorizationCodeGrant { get; set; } = false;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using Swashbuckle.AspNetCore.SwaggerUI;

namespace Microsoft.AspNetCore.Builder
Expand Down Expand Up @@ -42,8 +41,10 @@ public static void InjectJavascript(this SwaggerUIOptions options, string path,
/// <param name="name">The description that appears in the document selector drop-down</param>
public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name)
{
var urls = new List<UrlDescriptor>(options.ConfigObject.Urls ?? Enumerable.Empty<UrlDescriptor>());
urls.Add(new UrlDescriptor { Url = url, Name = name });
var urls = new List<UrlDescriptor>(options.ConfigObject.Urls ?? [])
{
new() { Url = url, Name = name }
};
options.ConfigObject.Urls = urls;
}

Expand Down
22 changes: 12 additions & 10 deletions test/WebSites/OAuth2Integration/AuthServer/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ internal static IEnumerable<Client> Clients()
ClientId = "test-id",
ClientName = "Test client (Code with PKCE)",

RedirectUris = new[] {
RedirectUris =
[
"http://localhost:55202/resource-server/swagger/oauth2-redirect.html", // IIS Express
"http://localhost:5000/resource-server/swagger/oauth2-redirect.html", // Kestrel
},
"http://localhost:5000/resource-server/swagger/oauth2-redirect.html", // Kestrel (HTTP)
"https://localhost:5001/resource-server/swagger/oauth2-redirect.html", // Kestrel (HTTPS)
],

ClientSecrets = { new Secret("test-secret".Sha256()) },
RequireConsent = true,

AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowedScopes = new[] { "readAccess", "writeAccess" },
AllowedScopes = ["readAccess", "writeAccess"],
};
}

Expand All @@ -33,25 +35,25 @@ internal static IEnumerable<ApiResource> ApiResources()
{
Name = "api",
DisplayName = "API",
Scopes = new[]
{
Scopes =
[
new Scope("readAccess", "Access read operations"),
new Scope("writeAccess", "Access write operations")
}
]
};
}

internal static List<TestUser> TestUsers()
{
return new List<TestUser>
{
return
[
new TestUser
{
SubjectId = "joebloggs",
Username = "joebloggs",
Password = "pass123"
}
};
];
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using IdentityServer4.Stores;
using IdentityServer4.Services;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Mvc;

namespace OAuth2Integration.AuthServer.Controllers
{
Expand Down
6 changes: 0 additions & 6 deletions test/WebSites/OAuth2Integration/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace OAuth2Integration
{
Expand Down
24 changes: 12 additions & 12 deletions test/WebSites/OAuth2Integration/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55202",
"sslPort": 0
}
},
{
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
Expand All @@ -21,10 +12,19 @@
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "resource-server/swagger",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"applicationUrl": "https://localhost:5001"
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55202",
"sslPort": 0
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -28,9 +27,10 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
.Concat(context.MethodInfo.DeclaringType.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>()
.Select(attr => attr.Policy)
.Where(p => p != null)
.Distinct();

var requiredScopes = requiredPolicies.Select(p => _authorizationOptions.GetPolicy(p))
var requiredScopes = requiredPolicies.Select(_authorizationOptions.GetPolicy)
.SelectMany(r => r.Requirements.OfType<ClaimsAuthorizationRequirement>())
.Where(cr => cr.ClaimType == "scope")
.SelectMany(r => r.AllowedValues)
Expand All @@ -47,13 +47,13 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};

operation.Security = new List<OpenApiSecurityRequirement>
{
operation.Security =
[
new OpenApiSecurityRequirement
{
[ oAuthScheme ] = requiredScopes
}
};
];
}
}
}
Expand Down
Loading

0 comments on commit 65a8035

Please sign in to comment.