Defaulting to the Latest API Version on Swagger UI - asp.net-core

I have implemented Swashbuckle.AspNetCore.SwaggerUI (version 6.1.4) in my API. The API also configures Microsoft.AspNetCore.Mvc.Versioning (version 5.0.0).
This is now working, and I can set the API version at the top of the page to resolve to the various exposed endpoints.
My question is, can I configure the Swagger UI to default the version drop-down list at the top-right of the Swagger page to the highest API version. Currently, this is displayed as follows:
Select a definition - V1.0 (default)
V1.1
V2.0
I would like the UI to default, in this case to the latest published version (V2.0)
Here is my code:
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, SwaggerOptions>();
services.AddSwaggerGen(
options =>
{
options.EnableAnnotations();
var xmlCommentsPath = GetXmlCommentsFilePath();
if (File.Exists(xmlCommentsPath))
{
options.IncludeXmlComments(xmlCommentsPath);
}
options.AddSecurityDefinition(
"Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme."
});
options.AddSecurityRequirement(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
public class SwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerOptions"/> class.
/// </summary>
/// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
public SwaggerOptions(
IApiVersionDescriptionProvider provider)
{
this.provider = provider;
}
/// <inheritdoc />
public void Configure(
SwaggerGenOptions options)
{
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}
private static OpenApiInfo CreateInfoForApiVersion(
ApiVersionDescription description)
{
var info = new OpenApiInfo
{
Title = "Demo API",
Version = description.ApiVersion.ToString(),
Description = "A sample application with Swagger, Swashbuckle, and API versioning.",
Contact = new OpenApiContact {Name = "DemoApi"},
License = new OpenApiLicense {Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT")}
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
public static class SwaggerConfigExtension
{
public static IApplicationBuilder
ConfigureSwaggerUi(
this IApplicationBuilder app,
IApiVersionDescriptionProvider apiVersionProvider)
{
return app.UseSwaggerUI(
options =>
{
options.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.Full);
foreach (var description in apiVersionProvider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
}
);
}
}
Any help/direction would be appreciated.

Install nuget Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
Then in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider apiVersionDescriptionProvider)
{
.....
app.UseSwaggerUI(c =>
{
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions.Reverse())
{
// Create the Swagger endpoints for each version
c.SwaggerEndpoint($"/swagger/" +
$"LibraryOpenAPISpecification{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
c.RoutePrefix = ""; // swagger UI at the root index.html
});
....
}
see this answer: click here

In my case, I simply needed to sort the values in my SwaggerConfigExtension method returned from the IApiVersionDescriptionProvider. Here is my code with the Swagger drop-down list presenting versions in descending (latest to oldest) order.
public static class SwaggerConfigExtension
{
public static IApplicationBuilder
ConfigureSwaggerUi(
this IApplicationBuilder app,
IApiVersionDescriptionProvider apiVersionProvider)
{
return app.UseSwaggerUI(
options =>
{
options.DocExpansion(
Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.List);
// Present API version in descending order
var versionDescriptions = apiVersionProvider
.ApiVersionDescriptions
.OrderByDescending(desc => desc.ApiVersion)
.ToList();
foreach (var description in versionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
}
);
}
}

Related

Swashbuckle extension version switch

I am new to asp.net core and I am using swagger. I downloaded it by following the steps Install-Package Swashbuckle.AspNetCore -Version 5.6.3.Then add middleware
services.AddSwaggerGen();Then add app.UseSwagger(c =>
{
c.SerializeAsV2 = true;
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
}); to the ge
nerated JSON document and Swagger UI.
Finally add the header information as per the documentation.
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = new Uri("https://twitter.com/spboyer"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url = new Uri("https://example.com/license"),
}
});
});
These are all requirements to follow the documentation. But I need to extend the requirements now, I need to add the version information of the API, similar to the API V1 API V2 version switch. I have referenced some sources but don't have the complete code, can you guys help me? Any help is excellent! !
Are you trying to switch versions like this?
First I created 2 versions of folders and controllers.Therefore, the namespace of each controller corresponds to its folder, like this:
V1 version:
namespace WebApplication129.Controllers.V1
{
[ApiController]
[Route("api/v1/[controller]")]
public class HomeController : ControllerBase
{
[Route("test")]
[HttpGet]
public string Test()
{
return "v1 test";
}
}
}
V2 version:
namespace WebApplication129.Controllers.V2
{
[ApiController]
[Route("api/v2/[controller]")]
public class HomeController : ControllerBase
{
[Route("test")]
[HttpGet]
public string Test()
{
return "v2 test";
}
}
}
Then create an agreement to inform Swagger, in this way, we can control how Swagger generates Swagger documents, thereby controlling the UI.
Create the following class:
public class GroupingByNamespaceConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
var controllerNamespace = controller.ControllerType.Namespace;
var apiVersion = controllerNamespace.Split(".").Last().ToLower();
if (!apiVersion.StartsWith("v")) { apiVersion = "v1"; }
controller.ApiExplorer.GroupName = apiVersion;
}
}
Now we must apply the convention. For that we go to AddControllers in ConfigureServices and add the convention:
services.AddControllers(options =>
{
options.Conventions.Add(new GroupingByNamespaceConvention());
});
The final complete startup.cs configuration is as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using System;
using WebApplication129.Controllers.conf;
namespace WebApplication129
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.Conventions.Add(new GroupingByNamespaceConvention());
});
services.AddSwaggerGen(config =>
{
var titleBase = "Test API";
var description = "This is a Web API for Test operations";
var TermsOfService = new Uri("https://xxxxxx");
var License = new OpenApiLicense()
{
Name = "Test"
};
var Contact = new OpenApiContact()
{
Name = "Test",
Email = "Test#hotmail.com",
Url = new Uri("https://xxxxxx")
};
config.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = titleBase + " v1",
Description = description,
TermsOfService = TermsOfService,
License = License,
Contact = Contact
});
config.SwaggerDoc("v2", new OpenApiInfo
{
Version = "v2",
Title = titleBase + " v2",
Description = description,
TermsOfService = TermsOfService,
License = License,
Contact = Contact
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(config =>
{
config.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
config.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2");
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

How do I reflect a dotnet web api endpoint that uses query string parameters in SwagggerUI?

I'm trying to implement a dotnet web api with API versioning that uses query strings and headers. Here im using swagger to document and test the endpoints. I successfully used path versioning and reflected the endpoints in swagger. But im struggling to understand how to reflect query string & header versioning in swagger. I tried to find a solution from this article https://swagger.io/docs/specification/describing-parameters/#query-parameters but was still confused how to implement this in my dotnet web api.
My project contains 2 main controller classes with the following API versions.
WeatherForecastController.cs
namespace QueryStringVersioning.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1", Deprecated = true)]
[ApiVersion("3.0")]
[Route ("api")] //support query string & header versioning
// [Route("api/v{version:apiVersion}/[controller]")] //support path versioning
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering",
"Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
[HttpGet, MapToApiVersion("3.0")]
public IActionResult GetV3_0() => Ok(new string[] { "MapToApiVersion value 3.0" });
[HttpGet, MapToApiVersion("1.1")]
public IActionResult GetV1_1() => Ok(new string[] { "Depreceated MapToApiVersion value" });
}}
WeatherForecastController2.cs
namespace QueryStringVersioning.Controllers2
{
[ApiController]
[ApiVersion("2.0")]
[ApiVersion("2.1")]
[Route ("api")] //support query string & header versioning
// [Route("api/v{version:apiVersion}/[controller]")] //support path versioning
public class WeatherForecastController : ControllerBase
{
public IActionResult GetV2_0() => Ok(new string[] { "This is API Version 2.0" });
[HttpGet, MapToApiVersion("2.1")]
public IActionResult GetV2_1() => Ok(new string[] { "This is API Version 2.1" });
}}
And the startup.cs file
namespace QueryStringVersioning
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v1",
Title = "API_Versioning V1",
});
c.SwaggerDoc("v1.1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v1.1",
Title = "API_Versioning V1.1",
});
c.SwaggerDoc("v2", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v2",
Title = "API_Versioning V2"
});
c.SwaggerDoc("v2.1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v2.1",
Title = "API_Versioning V2.1"
});
c.SwaggerDoc("v3", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v3",
Title = "API_Versioning V3"
});
c.ResolveConflictingActions (apiDescriptions => apiDescriptions.First ());
// c.OperationFilter<RemoveVersionFromParameter>();
// c.DocumentFilter<ReplaceVersionWithExactValueInPath>();
});
services.AddControllers();
services.AddMvc();
services.AddApiVersioning(option =>
{
option.ReportApiVersions = true;
option.AssumeDefaultVersionWhenUnspecified = true;
option.DefaultApiVersion = new ApiVersion(1, 0);
// Supporting multiple versioning scheme
option.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("X-version"), new QueryStringApiVersionReader("api-version"));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API_Versioning V1.0");
c.SwaggerEndpoint("/swagger/v1.1/swagger.json", "API_Versioning V1.1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "API_Versioning V2.0");
c.SwaggerEndpoint("/swagger/v2.1/swagger.json", "API_Versioning V2.1");
c.SwaggerEndpoint("/swagger/v3/swagger.json", "API_Versioning V3.0");
});
}
}
}
#michael-wang is correct. You need to also include the API Versioning API Explorer extensions. This extensions make API discovery API version aware. One of the many possible uses for this information is OpenAPI/Swagger integration.
Links to all of the applicable NuGet packages are listed on the API Versioning landing page. There is also an end-to-end example provided using Swashbuckle.

.net 4.6 web api2 401 Unauthorized with identity server 4

I have already a working identity server 4 in a .net core application.
namespace IdentityServer
{
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("myresourceapi", "My Resource API")
{
Scopes = {new Scope("apiscope")}
}
};
}
public static IEnumerable<Client> GetClients()
{
return new[]
{
// for public api
new Client
{
ClientId = "secret_client_id",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "apiscope" }
}
};
}
}
}
namespace IdentityServer
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddOperationalStore(options =>
{
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30; // interval in seconds
})
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
}
The problem is that now I need to make authenticated requests to a .net 4.6 web api2 (not core). And the IdentityServer4.AccessTokenValidation package doesn't work for that.
According to this question (https://stackoverflow.com/questions/41992272/is-it-possible-to-use-identity-server-4-running-on-net-core-with-a-webapi-app-r) all I have to do is to use the same package that was used for Identity server 3(IdentityServer3.AccessTokenValidation).
This is the code i have implemented in the webapi 2
using IdentityServer3.AccessTokenValidation;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Host.SystemWeb;
using IdentityModel.Extensions;
using System.Web.Http;
[assembly: OwinStartup(typeof(WebApplication10.Startup))]
namespace WebApplication10
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44357",
// For access to the introspection endpoint
ClientId = "secret_client_id",
ClientSecret = "secret".ToSha256(),
RequiredScopes = new[] { "apiscope" }
});
}
}
}
namespace WebApplication10.Controllers
{
public class ValuesController : ApiController
{
[Authorize]
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
}
The status i get all the time is 401 Unauthorized.
Am i doing something wrong?
Any help with this?
Thanks.
Without logs can't be sure what is issue in your case, but here is couple of fixes I made to make it work:
On Statup.cs class of IdentityServer project
Change AccessTokenJwtType to JWT, default value on IdentityServer4 is at+jwt but .Net Framework Api (OWIN/Katana) requires JWT.
Add /resources aud by setting EmitLegacyResourceAudienceClaim to true, this is removed on IdentityServer4.
You can verify the access_token on https://jwt.ms/ by checking "typ" and "aud" .
var builder = services.AddIdentityServer(
options =>
{
options.AccessTokenJwtType = "JWT";
options.EmitLegacyResourceAudienceClaim = true;
});
On Statup.cs class of .Net Framework Api project, set ValidationMode to ValidationMode.Local, custom access token validation endpoint used by this method is removed on IdentityServer4.
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44357",
// For access to the introspection endpoint
ClientId = "secret_client_id",
ClientSecret = "secret".ToSha256(),
RequiredScopes = new[] { "apiscope" },
ValidationMode = ValidationMode.Local,
});
I have sample working implementation here
I strongly suggest you to gather logs on API, this helps to find the actual issue in your case and finding the fix. here is a sample to turn on OWIN log on Api.
You can follow the sample from CrossVersionIntegrationTests .
Identity server 4 doesn't have the connect/accesstokenvalidation endpoint . So in identity server4 app , you can modify your ApiResource to add the ApiSecret :
new ApiResource("api1", "My API"){ ApiSecrets = new List<Secret> {new Secret("scopeSecret".Sha256())}}
And in your web api , config the IdentityServerBearerTokenAuthenticationOptions like :
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
ValidationMode = ValidationMode.ValidationEndpoint,
ClientId = "api1",
ClientSecret = "scopeSecret",
RequiredScopes = new[] { "api1" }
});
ClientId & ClientSecret are both from your ApiResource .

Why is api version not applied?

I have the following code Statup.cs to setup web api with swagger and multiple versions. The problem is that the version is not used - see screenshot below.
I did used AddApiVersioning .. also UrlSegmentApiVersionReader as ApiVersionReader in the configuration options.
What am I missing ?
The framework I used is .NetCore 3.0.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using System;
using System.IO;
using System.Reflection;
namespace SwaggerUI
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Employee API",
Version = "v1",
Description = "An API to perform Employee operations",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "John Walkner",
Email = "John.Walkner#gmail.com",
Url = new Uri("https://twitter.com/jwalkner"),
},
License = new OpenApiLicense
{
Name = "Employee API LICX",
Url = new Uri("https://example.com/license"),
}
});
c.SwaggerDoc("v2", new OpenApiInfo
{
Title = "Employee API",
Version = "v2",
Description = "An API to perform Employee operations",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "John Walkner",
Email = "John.Walkner#gmail.com",
Url = new Uri("https://twitter.com/jwalkner"),
},
License = new OpenApiLicense
{
Name = "Employee API LICX",
Url = new Uri("https://example.com/license"),
}
});
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
services.AddControllers();
services.AddApiVersioning(o => {
o.ReportApiVersions = true;
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
o.ApiVersionReader = new UrlSegmentApiVersionReader();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger(x => x.SerializeAsV2 = true);
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "My API V2");
});
}
}
}
I found the answer:
after services.AddApiVersioning I added services.AddVersionedApiExplorer and before I needed to add a reference to Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
services.AddVersionedApiExplorer(o =>
{
o.GroupNameFormat = "'v'VVV";
o.SubstituteApiVersionInUrl = true; // this is needed to work
});
See code here: https://github.com/LucaGabi/SwaggerUI
Also see here https://github.com/LucaGabi/WebApplication1 more complex setup.

Configure cors to allow all subdomains using ASP.NET Core (Asp.net 5, MVC6, VNext)

I have cors setup correctly in an ASP.NET Core web app. Im using the following package...
"Microsoft.AspNet.Cors": "6.0.0-rc1-final"
and here is the startup.cs snippet...
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddCors
(
options =>
{
options.AddPolicy
(
CORSDefaults.PolicyName,
builder =>
{
//From config...
var allowedDomains = new []{"http://aaa.somewhere.com","https://aaa.somewhere.com","http://bbb.somewhere.com","https://bbb.somewhere.com"};
//Load it
builder
.WithOrigins(allowedDomains)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}
);
}
);
}
This works great except that the list of subdomains to allow is growing fast and I want to allow all subdomains of "somewhere.com". Something like "*.somewhere.com". I cant seem to find any documentation on how to do this in the new ASP.NET Core (MVC6, ASP.NET5, VNext). All the docs/examples I'm finding that demonstrate how to do this are for earlier versions of MVC or WebApi. How can I achieve this in the new stack?
This has now been implemented in version 2.0.0. In your ConfigureServices use the following:
options.AddPolicy("MyCorsPolicy",
builder => builder
.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins("https://*.mydomain.com")
.AllowAnyMethod()
.AllowCredentials()
.AllowAnyHeader()
.Build()
);
Also, don't forget to call UseCors in your Configure call too:
app.UseCors("MyCorsPolicy");
I submitted a pull request to the ASP.NET team with this change so hopefully it will make it into the nuget package. Until then, I use this workaround.
Below you register cors as usual with the exception of having to register the WildCardCorsService class in the di container.
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Transient<ICorsService, WildCardCorsService>());
services.AddCors
(
options =>
{
options.AddPolicy
(
CORSDefaults.PolicyName,
builder =>
{
builder
.WithOrigins("http://*.withwildcardsubdomain.com", "http://nowildcard.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}
);
}
);
}
Save this class locally in your solution. It is a copy and edit of the Microsoft.AspNet.Cors.CorsService.cs class to allow it to handle wildcard subdomains. If it finds a wildcard char '*' it will check if the root domain matches on allowed origins and actual origin. It does NOT support partial wildcard matching.
namespace Microsoft.AspNet.Cors.Infrastructure
{
/// <summary>
/// This ICorsService should be used in place of the official default CorsService to support origins
/// like http://*.example.comwhich will allow any subdomain for example.com
/// </summary>
public class WildCardCorsService : ICorsService
{
private readonly CorsOptions _options;
/// <summary>
/// Creates a new instance of the <see cref="CorsService"/>.
/// </summary>
/// <param name="options">The option model representing <see cref="CorsOptions"/>.</param>
public WildCardCorsService(IOptions<CorsOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;
}
/// <summary>
/// Looks up a policy using the <paramref name="policyName"/> and then evaluates the policy using the passed in
/// <paramref name="context"/>.
/// </summary>
/// <param name="requestContext"></param>
/// <param name="policyName"></param>
/// <returns>A <see cref="CorsResult"/> which contains the result of policy evaluation and can be
/// used by the caller to set appropriate response headers.</returns>
public CorsResult EvaluatePolicy(HttpContext context, string policyName)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var policy = _options.GetPolicy(policyName);
return EvaluatePolicy(context, policy);
}
/// <inheritdoc />
public CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
}
var corsResult = new CorsResult();
var accessControlRequestMethod = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestMethod];
if (string.Equals(context.Request.Method, Microsoft.AspNet.Cors.Infrastructure.CorsConstants.PreflightHttpMethod, StringComparison.Ordinal) &&
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
{
EvaluatePreflightRequest(context, policy, corsResult);
}
else
{
EvaluateRequest(context, policy, corsResult);
}
return corsResult;
}
public virtual void EvaluateRequest(HttpContext context, CorsPolicy policy, CorsResult result)
{
var origin = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.Origin];
if (!OriginIsAllowed(origin, policy))
{
return;
}
AddOriginToResult(origin, policy, result);
result.SupportsCredentials = policy.SupportsCredentials;
AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders);
}
public virtual void EvaluatePreflightRequest(HttpContext context, CorsPolicy policy, CorsResult result)
{
var origin = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.Origin];
if (!OriginIsAllowed(origin, policy))
{
return;
}
var accessControlRequestMethod = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestMethod];
if (StringValues.IsNullOrEmpty(accessControlRequestMethod))
{
return;
}
var requestHeaders =
context.Request.Headers.GetCommaSeparatedValues(Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestHeaders);
if (!policy.AllowAnyMethod && !policy.Methods.Contains(accessControlRequestMethod))
{
return;
}
if (!policy.AllowAnyHeader &&
requestHeaders != null &&
!requestHeaders.All(header => Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase) ||
policy.Headers.Contains(header, StringComparer.OrdinalIgnoreCase)))
{
return;
}
AddOriginToResult(origin, policy, result);
result.SupportsCredentials = policy.SupportsCredentials;
result.PreflightMaxAge = policy.PreflightMaxAge;
result.AllowedMethods.Add(accessControlRequestMethod);
AddHeaderValues(result.AllowedHeaders, requestHeaders);
}
/// <inheritdoc />
public virtual void ApplyResult(CorsResult result, HttpResponse response)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
var headers = response.Headers;
if (result.AllowedOrigin != null)
{
headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowOrigin] = result.AllowedOrigin;
}
if (result.VaryByOrigin)
{
headers["Vary"] = "Origin";
}
if (result.SupportsCredentials)
{
headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowCredentials] = "true";
}
if (result.AllowedMethods.Count > 0)
{
// Filter out simple methods
var nonSimpleAllowMethods = result.AllowedMethods
.Where(m =>
!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleMethods.Contains(m, StringComparer.OrdinalIgnoreCase))
.ToArray();
if (nonSimpleAllowMethods.Length > 0)
{
headers.SetCommaSeparatedValues(
Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowMethods,
nonSimpleAllowMethods);
}
}
if (result.AllowedHeaders.Count > 0)
{
// Filter out simple request headers
var nonSimpleAllowRequestHeaders = result.AllowedHeaders
.Where(header =>
!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
.ToArray();
if (nonSimpleAllowRequestHeaders.Length > 0)
{
headers.SetCommaSeparatedValues(
Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowHeaders,
nonSimpleAllowRequestHeaders);
}
}
if (result.AllowedExposedHeaders.Count > 0)
{
// Filter out simple response headers
var nonSimpleAllowResponseHeaders = result.AllowedExposedHeaders
.Where(header =>
!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleResponseHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
.ToArray();
if (nonSimpleAllowResponseHeaders.Length > 0)
{
headers.SetCommaSeparatedValues(
Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlExposeHeaders,
nonSimpleAllowResponseHeaders);
}
}
if (result.PreflightMaxAge.HasValue)
{
headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlMaxAge]
= result.PreflightMaxAge.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture);
}
}
protected virtual bool OriginIsAllowed(string origin, CorsPolicy policy)
{
if (!string.IsNullOrWhiteSpace(origin) &&
(policy.AllowAnyOrigin ||
policy.Origins.Contains(origin) ||
IsWildCardSubdomainMatch(origin, policy)))
return true;
return false;
}
private void AddOriginToResult(string origin, CorsPolicy policy, CorsResult result)
{
if (policy.AllowAnyOrigin)
{
if (policy.SupportsCredentials)
{
result.AllowedOrigin = origin;
result.VaryByOrigin = true;
}
else
{
result.AllowedOrigin = Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AnyOrigin;
}
}
else
{
result.AllowedOrigin = origin;
}
}
private static void AddHeaderValues(IList<string> target, IEnumerable<string> headerValues)
{
if (headerValues == null)
{
return;
}
foreach (var current in headerValues)
{
target.Add(current);
}
}
private bool IsWildCardSubdomainMatch(string origin, CorsPolicy policy)
{
var actualOriginUri = new Uri(origin);
var actualOriginRootDomain = GetRootDomain(actualOriginUri);
foreach (var o in policy.Origins)
{
if (!o.Contains("*"))
continue;
// 1) CANNOT USE System.Text.RegularExpression since it does not exist in .net platform 5.4 (which the Microsoft.AspNet.Cors project.json targets)
// 2) '*' char is not valid for creation of a URI object so we replace it just for this comparison
var allowedOriginUri = new Uri(o.Replace("*", "SOMELETTERS"));
if (allowedOriginUri.Scheme == actualOriginUri.Scheme &&
actualOriginRootDomain == GetRootDomain(allowedOriginUri))
return true;
}
return false;
}
private string GetRootDomain(Uri uri)
{
//Got this snippet here http://stackoverflow.com/questions/16473838/get-domain-name-of-a-url-in-c-sharp-net
var host = uri.Host;
int index = host.LastIndexOf('.'), last = 3;
while (index > 0 && index >= last - 3)
{
last = index;
index = host.LastIndexOf('.', last - 1);
}
return host.Substring(index + 1);
}
}
/// <summary>
/// Needed to copy these in since some of them are internal to the Microsoft.AspNet.Cors project
/// </summary>
public static class CorsConstants
{
/// <summary>The HTTP method for the CORS preflight request.</summary>
public static readonly string PreflightHttpMethod = "OPTIONS";
/// <summary>The Origin request header.</summary>
public static readonly string Origin = "Origin";
/// <summary>
/// The value for the Access-Control-Allow-Origin response header to allow all origins.
/// </summary>
public static readonly string AnyOrigin = "*";
/// <summary>The Access-Control-Request-Method request header.</summary>
public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
/// <summary>The Access-Control-Request-Headers request header.</summary>
public static readonly string AccessControlRequestHeaders = "Access-Control-Request-Headers";
/// <summary>The Access-Control-Allow-Origin response header.</summary>
public static readonly string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
/// <summary>The Access-Control-Allow-Headers response header.</summary>
public static readonly string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
/// <summary>The Access-Control-Expose-Headers response header.</summary>
public static readonly string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
/// <summary>The Access-Control-Allow-Methods response header.</summary>
public static readonly string AccessControlAllowMethods = "Access-Control-Allow-Methods";
/// <summary>The Access-Control-Allow-Credentials response header.</summary>
public static readonly string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
/// <summary>The Access-Control-Max-Age response header.</summary>
public static readonly string AccessControlMaxAge = "Access-Control-Max-Age";
internal static readonly string[] SimpleRequestHeaders = new string[4]
{
"Origin",
"Accept",
"Accept-Language",
"Content-Language"
};
internal static readonly string[] SimpleResponseHeaders = new string[6]
{
"Cache-Control",
"Content-Language",
"Content-Type",
"Expires",
"Last-Modified",
"Pragma"
};
internal static readonly string[] SimpleMethods = new string[3]
{
"GET",
"HEAD",
"POST"
};
}
}
Enjoy!
The out-of-the-box CorsService uses policy.Origins.Contains(origin) to evaluate a request. So, it does not look like there is a trivial way to do what you require, because the List must contain the origin. You could implement your own ICorsService, inherit what the out-of-the-box CorsService already provides, and tweak the methods to handle the *.mydomain.com wildcard.
Edit Here is what I accomplished using yo aspnet to generate a 1.0.0-rc1-update2 Web Api project. It works. Register your service in Startup.cs (see CorsServiceCollectionExtensions for details.)
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.TryAdd(
ServiceDescriptor.Transient<ICorsService, MyCorsService>());
services.TryAdd(
ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(minLevel: LogLevel.Verbose);
app.UseCors(corsPolictyBuilder =>
{
corsPolictyBuilder.WithOrigins("*.mydomain.com");
});
app.Run(async context =>
{
await context.Response.WriteAsync(
$"Is Cors? {context.Request.Headers.ContainsKey(CorsConstants.Origin)}");
});
}
}
Here is the service, awaiting your implementation. You can either copy/paste or inherit from CorsService.
public class MyCorsService : CorsService, ICorsService
{
private ILogger _logger;
public MyCorsService(IOptions<CorsOptions> options, ILogger<MyCorsService> logger)
: base(options)
{
_logger = logger;
_logger.LogInformation("MyCorsService");
}
public override void ApplyResult(
CorsResult result, HttpResponse response)
{
_logger.LogInformation("ApplyResult");
base.ApplyResult(result, response);
}
public override void EvaluateRequest(
HttpContext context, CorsPolicy policy, CorsResult result)
{
_logger.LogInformation("EvaluateRequest");
base.EvaluateRequest(context, policy, result);
}
public override void EvaluatePreflightRequest(
HttpContext context, CorsPolicy policy, CorsResult result)
{
_logger.LogInformation("EvaluatePreflightRequest");
base.EvaluatePreflightRequest(context, policy, result);
}
}
SetIsOriginAllowedToAllowWildcardSubdomains function works well when the wildcard character is specified at the first part of the subdomains for e.g. https:\\*.modules.features.releaseversion.companyname.com but the same function doesn't give the desired result when the wildcard character is specified at any other part of the subdomain for e.g. https:\\environment.modules.*.releaseversion.companyname.com or https:\\*.modules.*.releaseversion.companyname.com or https:\\environment.*.*.releaseversion.companyname.com
Below code is inspired from the #Shaun Luttin and #sjdirect code snippet
We just wanted to extend the behaviour of the Microsoft.AspNetCore.Cors.Infrastructure.CorsService class to enable usage of wildcard character specified anywhere in the URL
Below class performs the CORS check to allow wild card subdomains. Copy this class locally into the desired project. It's an extended version of the Microsoft.AspNetCore.Cors.Infrastructure.CorsService to enable wild card support for subdomains.
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using System;
using System.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Cors.Infrastructure
{
public class CORSServiceWildCardSupport : CorsService, ICorsService
{
private readonly CorsOptions _options;
private readonly ILogger _logger;
public CORSServiceWildCardSupport(IOptions<CorsOptions> options, ILoggerFactory loggerFactory) : base(options, loggerFactory)
{
_options = options.Value;
_logger = loggerFactory.CreateLogger<CorsService>();
}
public new virtual CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (policy == null)
{
throw new ArgumentNullException("policy");
}
if (policy.AllowAnyOrigin && policy.SupportsCredentials)
{
throw new ArgumentException(Resource.InsecureConfiguration, "policy");
}
IHeaderDictionary headers = context.Request.Headers;
StringValues origin = headers[CorsConstants.Origin];
bool num = string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase);
bool flag = num && headers.ContainsKey(CorsConstants.AccessControlRequestMethod);
CorsResult result = new CorsResult
{
IsPreflightRequest = flag,
IsOriginAllowed = IsWildCardSubdomainMatch(origin, policy)
};
if (flag)
{
EvaluatePreflightRequest(context, policy, result);
}
else
{
EvaluateRequest(context, policy, result);
}
return result;
}
protected virtual IsWildCardSubdomainMatch(string origin, CorsPolicy policy)
{
var actualOrigin = new Uri(origin);
foreach (var o in policy.Origins)
{
if (IsWildcardMatch(actualOrigin, o))
{
return true;
}
}
return false;
}
private bool IsWildcardMatch(Uri actualOrigin, string wildcardUri)
{
if (!wildcardUri.StartsWith(actualOrigin.Scheme))
{
return false;
}
var wildcardUriMinusScheme = wildcardUri.Replace(actualOrigin.Scheme + "://", "");
var regexFirstStage = wildcardUriMinusScheme.Replace(".", "\\.");
var regexAllowedHosts = "^" + regexFirstStage.Replace("*", ".*") + "$";
var actualOriginMinusScheme = actualOrigin.OriginalString.Replace(actualOrigin.Scheme + "://", "");
var isMatch = Regex.IsMatch(actualOriginMinusScheme, regexAllowedHosts);
return isMatch;
}
}
}
From the above class function IsWildCardSubdomainMatch or IsWildcardMatch can be extended based on the requirement, for our requirement we just needed to perform string comparison.
Register the CORSServiceWildCardSupport class into the dependency container using the below extension class. Copy the class locally into the desired project
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.DependencyInjection
{
public static class CORSServiceCollectionExtensions
{
public static IServiceCollection AddCORSWithWildCardSupport(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
services.AddOptions();
services.TryAdd(ServiceDescriptor.Transient<ICorsService, CORSServiceWildCardSupport>());
services.TryAdd(ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());
return services;
}
public static IServiceCollection AddCORSWithWildCardSupport(this IServiceCollection services, Action<CorsOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
if (setupAction == null)
{
throw new ArgumentNullException("setupAction");
}
services.AddCORSWithWildCardSupport();
services.Configure(setupAction);
return services;
}
}
}
Register the CORS from the Startup class
public void ConfigureServices(IServiceCollection services)
{
try
{
string[] whitelist = {"https:\\environment.modules.*.releaseversion.companyname.com","https:\\*.modules.*.releaseversion.companyname.com","https:\\environment.*.*.releaseversion.companyname.com"};
services.AddCORSWithWildCardSupport(o => o.AddPolicy(Resource.CorsPolicyName, builder =>
{
builder.WithOrigins(whitelist)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
services.AddControllers();
services.AddMvc(option => option.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddAuthentication("Windows");
}
catch(Exception ex)
{
Logger.Error($"Failed to start due to {ex.Message}.");
}
}
services.AddControllers method also registers ICORSService into the dependency container hence always use AddCORS prior to AddControllers
Enjoy :)