Files
components/LiteCharms.Features/Extensions/Api.cs
T
Khwezi Mngoma 6418d27f5a
continuous-integration/drone/pr Build is passing
Added cookie policies on AddLiteCharmsWebSecurity
2026-06-14 12:50:13 +02:00

273 lines
10 KiB
C#

using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Api;
using LiteCharms.Features.Api.Configuration;
using LiteCharms.Features.Api.Sdk;
using LiteCharms.Features.Postgres;
namespace LiteCharms.Features.Extensions;
public static class Api
{
public const string Books = nameof(Books);
public const string Payments = nameof(Payments);
public static IServiceCollection AddPayfastServices(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(PayfastSettings));
services.Configure<PayfastSettings>(configSection);
return services;
}
public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(LiteCharmsClientSettings));
var authOptions = new LiteCharmsClientSettings();
configSection.Bind(authOptions);
services.Configure<LiteCharmsClientSettings>(configSection);
if (string.IsNullOrWhiteSpace(authOptions.Authority))
return services;
if (!authOptions.Authority.EndsWith("/", StringComparison.Ordinal)) authOptions.Authority += "/";
services.AddRefitClient<IConnectApi>()
.ConfigureHttpClient(config =>
{
config.BaseAddress = new Uri(authOptions.Authority);
config.Timeout = TimeSpan.FromSeconds(15);
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
});
services.AddScoped<TokenService>();
return services;
}
public static IServiceCollection AddLiteCharmsWebSecurity(this IServiceCollection services, IConfiguration configuration)
{
var certificate = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(configuration["DataProtection:Certificate"]!), configuration["DataProtection:Password"]);
services.AddDataProtection().PersistKeysToDbContext<DataProtectionDbContext>()
.ProtectKeysWithCertificate(certificate)
.SetApplicationName("LiteCharmsApp");
var configSection = configuration.GetSection(nameof(LiteCharmsSettings));
var authOptions = new LiteCharmsSettings();
configSection.Bind(authOptions);
services.Configure<LiteCharmsSettings>(configSection);
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.Name = "LiteCharmsApp.Session";
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = authOptions.Authority;
options.ClientId = authOptions.ClientId;
options.ClientSecret = authOptions.ClientSecret;
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
options.CorrelationCookie.SameSite = SameSiteMode.None;
options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
options.NonceCookie.SameSite = SameSiteMode.None;
options.ForwardSignOut = CookieAuthenticationDefaults.AuthenticationScheme;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = context =>
{
var idToken = context.ProtocolMessage.IdTokenHint;
if (string.IsNullOrEmpty(idToken))
{
var tokens = context.Properties.GetTokens();
var idTokenItem = tokens.FirstOrDefault(t => string.Equals(t.Name, "id_token", StringComparison.Ordinal));
if (idTokenItem != null) context.ProtocolMessage.IdTokenHint = idTokenItem.Value;
}
return Task.CompletedTask;
},
};
});
services.AddCascadingAuthenticationState();
return services;
}
public static IServiceCollection AddLiteCharmsApiSecurity(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(LiteCharmsSettings));
var authOptions = new LiteCharmsSettings();
configSection.Bind(authOptions);
services.Configure<LiteCharmsSettings>(configSection);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = authOptions.Authority;
options.Audience = authOptions.Audience;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = authOptions.Authority,
ValidateAudience = true,
ValidateIssuer = true,
};
});
services.AddAuthorization();
return services;
}
public static WebApplication AddSecurityEndpoints(this WebApplication app)
{
app.MapGet("/login", async (HttpContext context, string redirectUri = "/") =>
{
await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
{
RedirectUri = redirectUri,
});
});
app.MapGet("/logout", async (HttpContext context, string? redirectUri = null) =>
{
var idToken = await context.GetTokenAsync("id_token");
if (string.IsNullOrWhiteSpace(redirectUri))
{
var host = context.Request.Host.ToUriComponent();
redirectUri = $"https://{host}/";
}
var authProperties = new AuthenticationProperties { RedirectUri = redirectUri, };
if (!string.IsNullOrEmpty(idToken))
authProperties.Parameters.Add("id_token_hint", idToken);
await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
});
return app;
}
public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddHttpClient();
services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1);
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),
new QueryStringApiVersionReader("version"),
new QueryStringApiVersionReader("version"),
new MediaTypeApiVersionReader("version"));
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"];
var healthUrl = "http://localhost:8080/health";
if (!string.IsNullOrWhiteSpace(urls))
{
string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))!
.Replace("0.0.0.0", "localhost")
.Replace("*", "localhost")
.Replace("+", "localhost");
healthUrl = $"{firstUrl.TrimEnd('/')}/health";
}
services.AddHealthChecksUI(setup =>
{
setup.SetNotifyUnHealthyOneTimeUntilChange();
setup.AddHealthCheckEndpoint("primary, heal", healthUrl);
setup.SetHeaderText("Midrand Books");
})
.AddInMemoryStorage();
services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Cache());
options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
});
services.AddOpenApi(options => options.AddDocumentTransformer<OpenApiBearerSecuritySchemeTransformer>());
return services;
}
public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary<int, RouteGroupBuilder> versionGroups)
{
var endpoints = app.Services.GetRequiredService<IEnumerable<IEndpoint>>();
foreach (var endpoint in endpoints)
{
var versionAttributes = endpoint.GetType().GetCustomAttributes<ApiVersionTargetAttribute>().ToList();
if (versionAttributes.Count != 0)
{
foreach (var attr in versionAttributes)
if (versionGroups.TryGetValue(attr.MajorVersion, out var targetGroup))
endpoint.Map(targetGroup);
}
else
endpoint.Map(app);
}
return app;
}
public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly)
{
ServiceDescriptor[] discriptors = [.. assembly.DefinedTypes
.Where(t => t is { IsInterface: false, IsAbstract: false })
.Where(t => t.IsAssignableTo(typeof(IEndpoint)))
.Select(t => ServiceDescriptor.Transient(typeof(IEndpoint), t))];
services.TryAddEnumerable(discriptors);
return services;
}
public static string ToEndpointName(this Type target, string? annotation = "") =>
$"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture);
}