forked from gongxuegit/tiku-backend.net
feat(security): add distributed authorization foundation
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Tiku.Api.OpenApi;
|
||||
using Tiku.Api.Security;
|
||||
|
||||
namespace Tiku.Api.Configuration;
|
||||
|
||||
@@ -7,7 +8,8 @@ internal static class ApiPresentationExtensions
|
||||
{
|
||||
internal static IServiceCollection AddApiPresentation(this IServiceCollection services)
|
||||
{
|
||||
services.AddControllers()
|
||||
services.AddControllers(options =>
|
||||
options.Conventions.Add(new EndpointAuthorizationMetadataConvention()))
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
@@ -29,6 +29,7 @@ public static class ApplicationBuilderExtensions
|
||||
app.UseRouting();
|
||||
app.UseCors(CorsOptions.PolicyName);
|
||||
app.UseMiddleware<TenantResolutionMiddleware>();
|
||||
app.UseMiddleware<BrowserCsrfMiddleware>();
|
||||
app.UseAuthentication();
|
||||
app.UseMiddleware<AuthRateLimitPartitionMiddleware>();
|
||||
app.UseRateLimiter();
|
||||
|
||||
@@ -29,6 +29,15 @@ internal static class AuthenticationExtensions
|
||||
var jwtOptions = configuration
|
||||
.GetSection("Security:Jwt")
|
||||
.Get<JwtOptions>() ?? new JwtOptions();
|
||||
services.AddOptions<BrowserAuthOptions>()
|
||||
.Bind(configuration.GetSection(BrowserAuthOptions.SectionName))
|
||||
.Validate(
|
||||
options => options.AllowedOrigins.All(origin =>
|
||||
Uri.TryCreate(origin, UriKind.Absolute, out var uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) &&
|
||||
string.IsNullOrEmpty(uri.PathAndQuery.Trim('/'))),
|
||||
"BrowserAuth AllowedOrigins must contain only HTTP(S) origins without paths.")
|
||||
.ValidateOnStart();
|
||||
|
||||
services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
@@ -84,11 +93,41 @@ internal static class AuthenticationExtensions
|
||||
};
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(context.Request.Headers.Authorization) &&
|
||||
IsSameOriginBrowserRequest(context.Request) &&
|
||||
context.Request.Cookies.TryGetValue(BrowserAuthOptions.AccessCookie, out var accessToken))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTokenValidated = ValidateTokenAsync,
|
||||
OnChallenge = WriteTenantConflictChallengeAsync
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsSameOriginBrowserRequest(HttpRequest request)
|
||||
{
|
||||
var source = request.Headers.Origin.ToString();
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
source = request.Headers.Referer.ToString();
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(source, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return string.Equals(uri.Scheme, request.Scheme, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(uri.Authority, request.Host.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return string.Equals(
|
||||
request.Headers["Sec-Fetch-Site"].ToString(),
|
||||
"same-origin",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static async Task ValidateTokenAsync(TokenValidatedContext context)
|
||||
{
|
||||
var principal = context.Principal;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Serilog;
|
||||
using Tiku.Application;
|
||||
using Tiku.Infrastructure;
|
||||
using Tiku.Infrastructure.Messaging;
|
||||
using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.Api.Configuration;
|
||||
|
||||
@@ -15,14 +17,51 @@ public static class DependencyInjection
|
||||
preserveStaticLogger: true);
|
||||
|
||||
builder.Services.AddApiPresentation();
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddNetworkConfiguration(builder.Configuration);
|
||||
builder.Services.AddNetworkConfiguration(builder.Configuration, builder.Environment);
|
||||
builder.Services.AddApiRateLimiting(builder.Configuration);
|
||||
|
||||
var connectionString = Options.OptionsValidation.ResolveDatabaseConnectionString(
|
||||
builder.Configuration,
|
||||
builder.Environment.IsDevelopment());
|
||||
builder.Services.AddInfrastructure(connectionString);
|
||||
var redisConnectionString = builder.Configuration.GetConnectionString("Redis") ?? builder.Configuration["REDIS_URL"];
|
||||
builder.Services.AddOptions<RedisSecurityConnectionOptions>()
|
||||
.Configure(options => options.ConnectionString = redisConnectionString ?? string.Empty)
|
||||
.Validate(
|
||||
options => !builder.Environment.IsProduction() || !string.IsNullOrWhiteSpace(options.ConnectionString),
|
||||
"Redis is required in Production.")
|
||||
.ValidateOnStart();
|
||||
if (!string.IsNullOrWhiteSpace(redisConnectionString))
|
||||
{
|
||||
builder.Services.AddRedisSecurity(redisConnectionString, builder.Environment.EnvironmentName);
|
||||
}
|
||||
else if (builder.Environment.IsProduction())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Redis is required in Production. Configure ConnectionStrings:Redis or REDIS_URL.");
|
||||
}
|
||||
var messaging = builder.Configuration.GetSection("RabbitMq").Get<MessagingOptions>() ?? new MessagingOptions();
|
||||
builder.Services.AddOptions<MessagingOptions>()
|
||||
.Bind(builder.Configuration.GetSection("RabbitMq"))
|
||||
.Validate(
|
||||
options => !builder.Environment.IsProduction() ||
|
||||
(options.IsConfigured &&
|
||||
!string.IsNullOrWhiteSpace(options.Username) &&
|
||||
!string.IsNullOrWhiteSpace(options.Password)),
|
||||
"Production RabbitMQ requires a valid Host, Username and Password.")
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddSingleton(messaging);
|
||||
if (messaging.IsConfigured)
|
||||
{
|
||||
messaging.ConfigureConsumers = false;
|
||||
builder.Services.AddReliableMessaging(messaging);
|
||||
}
|
||||
else if (builder.Environment.IsProduction())
|
||||
{
|
||||
throw new InvalidOperationException("RabbitMQ is required in Production. Configure RabbitMq:Host.");
|
||||
}
|
||||
|
||||
builder.Services.AddApiDataProtection(builder.Configuration, builder.Environment);
|
||||
builder.Services.AddExternalServiceOptions(builder.Configuration, builder.Environment);
|
||||
|
||||
@@ -9,10 +9,15 @@ internal static class NetworkConfigurationExtensions
|
||||
{
|
||||
internal static IServiceCollection AddNetworkConfiguration(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
IConfiguration configuration,
|
||||
IHostEnvironment environment)
|
||||
{
|
||||
services.AddOptions<TenantResolutionOptions>()
|
||||
.Bind(configuration.GetSection(TenantResolutionOptions.SectionName));
|
||||
.Bind(configuration.GetSection(TenantResolutionOptions.SectionName))
|
||||
.Validate(
|
||||
options => OptionsValidation.BeValidTenantResolutionOptions(options, configuration, environment.IsProduction()),
|
||||
"Production requires formal platform hosts, non-wildcard AllowedHosts, and trusted proxy addresses.")
|
||||
.ValidateOnStart();
|
||||
services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
|
||||
Reference in New Issue
Block a user