Skip to content

Instantly share code, notes, and snippets.

@DamianEdwards
Created August 6, 2022 00:32
Show Gist options
  • Save DamianEdwards/1620ce074cf714c2f7e222a7278753b3 to your computer and use it in GitHub Desktop.
Save DamianEdwards/1620ce074cf714c2f7e222a7278753b3 to your computer and use it in GitHub Desktop.
Some SwashBuckling with .NET 7 rc.1
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