Created
August 6, 2022 00:32
-
-
Save DamianEdwards/1620ce074cf714c2f7e222a7278753b3 to your computer and use it in GitHub Desktop.
Some SwashBuckling with .NET 7 rc.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Buffers.Text; | |
using System.Text; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
using Microsoft.AspNetCore.Authentication; | |
using Microsoft.AspNetCore.Authentication.JwtBearer; | |
using Microsoft.AspNetCore.Authorization; | |
using Microsoft.Extensions.Options; | |
using Microsoft.OpenApi.Models; | |
using Swashbuckle.AspNetCore.SwaggerGen; | |
var builder = WebApplication.CreateBuilder(args); | |
builder.Services.AddAuthentication().AddJwtBearer(); | |
builder.Services.AddAuthorization(); | |
builder.Services.ConfigureHttpJsonOptions(c => | |
{ | |
c.SerializerOptions.Converters.Add(new Int128Converter { SerializationFormat = Int128JsonFormat.StringAboveInt64 }); | |
}); | |
builder.Services.AddEndpointsApiExplorer(); | |
builder.Services.AddSwaggerGen(c => | |
{ | |
//c.InferSecuritySchemes(); | |
c.AddSecurityDefinition("Bearer", | |
new() | |
{ | |
Type = SecuritySchemeType.Http, | |
Scheme = "bearer", // "bearer" refers to the header name here | |
In = ParameterLocation.Header, | |
BearerFormat = "Json Web Token", | |
Description = "JWT Authorization header using the Bearer scheme" | |
}); | |
c.MapType<Int128>(() => new OpenApiSchema | |
{ | |
Type = "number", | |
Minimum = 0 | |
}); | |
c.MapType<DateOnly>(() => new OpenApiSchema | |
{ | |
Type = "string", | |
Format = "date" | |
}); | |
c.OperationFilter<AuthorizeOperationFilter>(); | |
c.OperationFilter<FixInt128ParametersOperationFilter>(); | |
c.DocumentFilter<AnonymousTypesDocumentFilter>(); | |
}); | |
var app = builder.Build(); | |
app.UseSwagger(); | |
app.UseSwaggerUI(); | |
app.UseHttpsRedirection(); | |
app.MapGet("/hello", (HttpContext context) => $"Hello, {context.User.Identity?.Name ?? "guest"}!"); | |
app.MapGet("/numbers/integer/{number}", (int number) => new { number }) | |
.WithName("NumbersInteger") | |
.WithOpenApi(); | |
app.MapGet("/numbers/long/{number}", (long number) => new { number }) | |
.WithName("NumbersLongs") | |
.WithOpenApi(); | |
app.MapGet("/numbers/decimal/{number}", (decimal number) => new { number }) | |
.WithName("NumbersDecimals") | |
.WithOpenApi(); | |
app.MapGet("/numbers/double/{number}", (double number) => new { number }) | |
.WithName("NumbersDoubles") | |
.WithOpenApi(); | |
app.MapGet("/numbers/int128/{number}", (Int128 number) => new { number, asLong = (long)number }) | |
.WithName("NumbersInt128") | |
.WithOpenApi(o => { | |
o.Description = $"Max Int128 is {Int128.MaxValue}"; | |
return o; | |
}); | |
app.MapGet("/authn", async (IAuthenticationSchemeProvider authenticationSchemeProvider) => | |
{ | |
var schemes = await authenticationSchemeProvider.GetAllSchemesAsync(); | |
return new | |
{ | |
defaults = new | |
{ | |
authenticate = (await authenticationSchemeProvider.GetDefaultAuthenticateSchemeAsync())?.Name ?? "[none]", | |
challenge = (await authenticationSchemeProvider.GetDefaultChallengeSchemeAsync())?.Name ?? "[none]", | |
signIn = (await authenticationSchemeProvider.GetDefaultSignInSchemeAsync())?.Name ?? "[none]", | |
signOut = (await authenticationSchemeProvider.GetDefaultSignOutSchemeAsync())?.Name ?? "[none]", | |
forbid = (await authenticationSchemeProvider.GetDefaultForbidSchemeAsync())?.Name ?? "[none]" | |
}, | |
schemes = schemes.Select(s => new { s.Name, HandlerTypeName = s.HandlerType.Name }) | |
}; | |
}) | |
.WithOpenApi(); | |
app.MapGet("/jwt", (IOptionsMonitor<JwtBearerOptions> jwt) => | |
{ | |
var options = jwt.Get("Bearer"); | |
return new | |
{ | |
options.ClaimsIssuer, | |
options.TokenValidationParameters.ValidAudiences, | |
options.TokenValidationParameters.ValidAudience, | |
options.TokenValidationParameters.ValidIssuers, | |
options.TokenValidationParameters.ValidIssuer | |
}; | |
}) | |
.WithName("GetJwtBearerOptions"); | |
var sampleResult = new { message = "a message" }; | |
app.MapGet("/message/1", () => sampleResult).WithName("GetMessage1"); | |
app.MapGet("/message/2", () => sampleResult).WithName("GetMessage2"); | |
var group = app.MapGroup("/protected"); | |
group.RequireAuthorization(); | |
group.MapGet("/hello", (HttpContext context) => $"Hello, {context.User.Identity?.Name ?? "[no user name]"}!"); | |
var summaries = new[] | |
{ | |
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" | |
}; | |
app.MapGet("/weatherforecast", () => | |
{ | |
var forecast = Enumerable.Range(1, 5).Select(index => | |
new WeatherForecast | |
( | |
DateOnly.FromDateTime(DateTime.Now.AddDays(index)), | |
Random.Shared.Next(-20, 55), | |
summaries[Random.Shared.Next(summaries.Length)] | |
)) | |
.ToArray(); | |
return forecast; | |
}) | |
.WithName("GetWeatherForecast") | |
.WithOpenApi(); | |
app.Run(); | |
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) | |
{ | |
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); | |
} | |
internal class Int128Converter : JsonConverter<Int128> | |
{ | |
public Int128JsonFormat SerializationFormat { get; init; } = Int128JsonFormat.Integer; | |
public override Int128 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |
{ | |
if (reader.TokenType == JsonTokenType.Number) | |
{ | |
Span<char> chars = stackalloc char[reader.ValueSpan.Length]; | |
reader.CopyString(chars); | |
return Int128.Parse(chars); | |
} | |
if (reader.TokenType == JsonTokenType.String && reader.GetString() is { } int128asString) | |
{ | |
return Int128.Parse(int128asString); | |
} | |
throw new JsonException(); | |
} | |
public override void Write(Utf8JsonWriter writer, Int128 value, JsonSerializerOptions options) | |
{ | |
if (SerializationFormat == Int128JsonFormat.Integer) | |
{ | |
if (value <= long.MaxValue) | |
{ | |
writer.WriteNumberValue((long)value); | |
return; | |
} | |
writer.WriteRawValue(value.ToString()); | |
return; | |
} | |
if (SerializationFormat == Int128JsonFormat.StringAboveInt64) | |
{ | |
if (value <= long.MaxValue) | |
{ | |
writer.WriteNumberValue((long)value); | |
return; | |
} | |
} | |
writer.WriteStringValue(value.ToString()); | |
} | |
} | |
internal enum Int128JsonFormat | |
{ | |
Integer, | |
StringAboveInt64, | |
String | |
} | |
internal class AnonymousTypesDocumentFilter : IDocumentFilter | |
{ | |
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) | |
{ | |
var schemas = swaggerDoc.Components.Schemas; | |
var renamedSchemas = new Dictionary<string, string>(); | |
foreach (var schemaId in schemas.Keys) | |
{ | |
if (schemaId.Contains("AnonymousType")) | |
{ | |
var schema = swaggerDoc.Components.Schemas[schemaId]; | |
var schemaRefs = GetSchemaRefsBySchemaId(schemaId, swaggerDoc).ToList(); | |
if (schemaRefs.Count == 1 && !string.IsNullOrEmpty(schemaRefs[0].Item1.OperationId)) | |
{ | |
// There's only a single operation returning this anonymous type so infer schema name from operation id | |
var (operation, responseSchemas) = schemaRefs[0]; | |
var newId = $"{operation.OperationId}_Result"; | |
// Update schemas | |
renamedSchemas[schemaId] = newId; | |
foreach (var responseSchema in responseSchemas) | |
{ | |
responseSchema.Reference = new() { Type = ReferenceType.Schema, Id = newId }; | |
} | |
} | |
} | |
} | |
foreach (var kvp in renamedSchemas) | |
{ | |
var schema = schemas[kvp.Key]; | |
schemas.Add(kvp.Value, schema); | |
schemas.Remove(kvp.Key); | |
} | |
} | |
private static IEnumerable<(OpenApiOperation, IEnumerable<OpenApiSchema>)> GetSchemaRefsBySchemaId(string schemaId, OpenApiDocument swaggerDoc) | |
{ | |
foreach (var path in swaggerDoc.Paths) | |
{ | |
foreach (var operation in path.Value.Operations) | |
{ | |
var schemaRefs = operation.Value.Responses.Select(kvp => kvp.Value) | |
.SelectMany(oar => oar.Content.Values.Select(v => v.Schema)) | |
.Where(s => s.Reference?.Id == schemaId); | |
if (schemaRefs.Any()) | |
{ | |
yield return (operation.Value, schemaRefs); | |
} | |
} | |
} | |
} | |
} | |
internal class FixInt128ParametersOperationFilter : IOperationFilter | |
{ | |
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |
{ | |
var int128Params = context.ApiDescription.ParameterDescriptions.Where(p => p.Type == typeof(Int128)); | |
foreach (var parameter in int128Params) | |
{ | |
var match = operation.Parameters.Single(p => p.Name == parameter.Name); | |
match.Schema = new() | |
{ | |
Type = "number", | |
Minimum = 0 | |
}; | |
} | |
} | |
} | |
internal class AuthorizeOperationFilter : IOperationFilter | |
{ | |
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; | |
private OpenApiSecurityScheme? _defaultScheme; | |
public AuthorizeOperationFilter(IAuthenticationSchemeProvider authenticationSchemeProvider) | |
{ | |
_authenticationSchemeProvider = authenticationSchemeProvider; | |
} | |
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |
{ | |
var authorizeMetadata = context.ApiDescription.ActionDescriptor.EndpointMetadata.OfType<IAuthorizeData>(); | |
var policyMetadata = context.ApiDescription.ActionDescriptor.EndpointMetadata.OfType<AuthorizationPolicy>(); | |
if (authorizeMetadata.Any() || policyMetadata.Any()) | |
{ | |
var schemes = | |
authorizeMetadata.SelectMany(m => m.AuthenticationSchemes?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList() ?? Enumerable.Empty<string>()) | |
.Concat(policyMetadata.SelectMany(m => m.AuthenticationSchemes)) | |
.ToList(); | |
if (schemes.Any()) | |
{ | |
var security = new List<OpenApiSecurityRequirement>(); | |
foreach (var scheme in schemes) | |
{ | |
var apiScheme = new OpenApiSecurityScheme | |
{ | |
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = scheme } | |
}; | |
security.Add(new() { [apiScheme] = Array.Empty<string>() }); | |
} | |
operation.Security = security; | |
} | |
else | |
{ | |
if (_defaultScheme is null) | |
{ | |
var defaultScheme = _authenticationSchemeProvider.GetDefaultAuthenticateSchemeAsync().Result; | |
_defaultScheme = new OpenApiSecurityScheme | |
{ | |
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = defaultScheme?.Name } | |
}; | |
} | |
operation.Security = new List<OpenApiSecurityRequirement> | |
{ | |
new() | |
{ | |
[_defaultScheme] = Array.Empty<string>() | |
} | |
}; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment