From 6526445dd4c109f6db93df91162199ff499d9f8c Mon Sep 17 00:00:00 2001 From: fiodarsazanavets Date: Thu, 11 Dec 2025 18:20:43 +0000 Subject: [PATCH] Add MailDev components --- .../OnlineShop.ApiService/AuthExtensions.cs | 52 ++++++++++++++++++ .../OnlineShop.ApiService.csproj | 1 + .../OnlineShop.ApiService/Program.cs | 22 ++++++-- .../OnlineShop/OnlineShop.AppHost/AppHost.cs | 8 ++- .../OnlineShop.AppHost.csproj | 11 +++- .../MailDevContainerImageTags.cs | 10 ++++ .../MailDevResource.cs | 21 +++++++ .../MailDevResourceBuilderExtensions.cs | 31 +++++++++++ .../OnlineShop.MailDev.Hosting.csproj | 13 +++++ .../OnlineShop.Web/AuthExtensions.cs | 55 +++++++++++++++++++ .../OnlineShop.Web/OnlineShop.Web.csproj | 5 ++ .../OnlineShop/OnlineShop.Web/Program.cs | 25 +++++++-- .../OnlineShop/OnlineShop.slnx | 1 + 13 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/AuthExtensions.cs create mode 100644 AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevContainerImageTags.cs create mode 100644 AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResource.cs create mode 100644 AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResourceBuilderExtensions.cs create mode 100644 AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/OnlineShop.MailDev.Hosting.csproj create mode 100644 AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/AuthExtensions.cs diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/AuthExtensions.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/AuthExtensions.cs new file mode 100644 index 0000000..d3af18e --- /dev/null +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/AuthExtensions.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace OnlineShop.ApiService +{ + public static class AuthExtensions + { + public static void ConfigureApiJwt(this AuthenticationBuilder authentication) + { + // Named options + authentication.Services + .AddOptions( + JwtBearerDefaults.AuthenticationScheme) + .Configure< + IConfiguration, + IHttpClientFactory, + IHostEnvironment>(Configure); + + // Unnamed options + authentication.Services.AddOptions() + .Configure< + IConfiguration, + IHttpClientFactory, + IHostEnvironment>(Configure); + + static void Configure( + JwtBearerOptions options, + IConfiguration configuration, + IHttpClientFactory httpClientFactory, + IHostEnvironment hostEnvironment) + { + var backchannelHttpClient = + httpClientFactory.CreateClient( + "OidcBackchannel"); + + options.Backchannel = backchannelHttpClient; + options.Authority = + backchannelHttpClient + .GetIdpAuthorityUri().ToString(); + options.RequireHttpsMetadata = + !hostEnvironment.IsDevelopment(); + options.MapInboundClaims = false; + options.TokenValidationParameters = + new TokenValidationParameters + { + ValidateAudience = false + }; + } + } + } +} diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj b/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj index 3681550..94b4456 100644 --- a/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj @@ -11,6 +11,7 @@ + diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/Program.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/Program.cs index 570ff49..19a9669 100644 --- a/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/Program.cs +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.ApiService/Program.cs @@ -1,17 +1,31 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using OnlineShop.ApiService; + var builder = WebApplication.CreateBuilder(args); -// Add service defaults & Aspire client integrations. builder.AddServiceDefaults(); -// Add services to the container. builder.Services.AddProblemDetails(); -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +builder.Services.AddHttpClient( + "OidcBackchannel", o => o.BaseAddress = new("http://idp")); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = + JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = + JwtBearerDefaults.AuthenticationScheme; + +}) +.AddJwtBearer() +.ConfigureApiJwt(); + + var app = builder.Build(); -// Configure the HTTP request pipeline. app.UseExceptionHandler(); if (app.Environment.IsDevelopment()) diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/AppHost.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/AppHost.cs index 8e8e083..e9d3ab7 100644 --- a/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/AppHost.cs +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/AppHost.cs @@ -1,13 +1,17 @@ using Microsoft.Extensions.Hosting; using OnlineShop.AppHost.Extensions; +using OnlineShop.MailDev.Hosting; var builder = DistributedApplication.CreateBuilder(args); +var maildev = builder.AddMailDev("maildev"); + var idp = builder.AddKeycloakContainer( "idp", tag: "23.0") .ImportRealms("Keycloak") .WithExternalHttpEndpoints(); +var cache = builder.AddRedis("cache"); var apiService = builder.AddProject("apiservice") .WithHttpHealthCheck("/health") @@ -21,7 +25,9 @@ var webFrontend = builder .WithHttpHealthCheck("/health") .WithReference(apiService) .WithReference(idp, env: "Identity__ClientSecret") - .WaitFor(idp); + .WaitFor(idp) + .WithReference(cache) + .WaitFor(cache); if (builder.Environment.IsDevelopment()) { diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj b/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj index 168c60f..ec5f46c 100644 --- a/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,11 +8,20 @@ 2f2ef062-99af-422f-b6a5-1094759553e7 + + + + + + + PreserveNewest diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevContainerImageTags.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevContainerImageTags.cs new file mode 100644 index 0000000..819192f --- /dev/null +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevContainerImageTags.cs @@ -0,0 +1,10 @@ +namespace OnlineShop.MailDev.Hosting; + +internal static class MailDevContainerImageTags +{ + internal const string Registry = "docker.io"; + + internal const string Image = "maildev/maildev"; + + internal const string Tag = "2.1.0"; +} diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResource.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResource.cs new file mode 100644 index 0000000..ba0ca73 --- /dev/null +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResource.cs @@ -0,0 +1,21 @@ +using Aspire.Hosting.ApplicationModel; + +namespace OnlineShop.MailDev.Hosting; + +public sealed class MailDevResource(string name) : + ContainerResource(name), IResourceWithConnectionString +{ + internal const string SmtpEndpointName = "smtp"; + internal const string HttpEndpointName = "http"; + + private EndpointReference? _smtpReference; + + public EndpointReference SmtpEndpoint => + _smtpReference ??= new(this, SmtpEndpointName); + + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create( + $"smtp://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)}" + ); +} + diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResourceBuilderExtensions.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResourceBuilderExtensions.cs new file mode 100644 index 0000000..09b7681 --- /dev/null +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/MailDevResourceBuilderExtensions.cs @@ -0,0 +1,31 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; + +namespace OnlineShop.MailDev.Hosting; + +public static class MailDevResourceBuilderExtensions +{ + public static IResourceBuilder AddMailDev( + this IDistributedApplicationBuilder builder, + string name, + int? httpPort = null, + int? smtpPort = null) + { + MailDevResource resource = new(name); + + return builder.AddResource(resource) + .WithImage(MailDevContainerImageTags.Image) + .WithImageRegistry(MailDevContainerImageTags.Registry) + .WithImageTag(MailDevContainerImageTags.Tag) + .WithHttpEndpoint( + targetPort: 1080, + port: httpPort, + name: MailDevResource.HttpEndpointName) + .WithEndpoint( + targetPort: 1025, + port: smtpPort, + name: MailDevResource.SmtpEndpointName); + + } + +} diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/OnlineShop.MailDev.Hosting.csproj b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/OnlineShop.MailDev.Hosting.csproj new file mode 100644 index 0000000..d809e49 --- /dev/null +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.MailDev.Hosting/OnlineShop.MailDev.Hosting.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/AuthExtensions.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/AuthExtensions.cs new file mode 100644 index 0000000..13e6c67 --- /dev/null +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/AuthExtensions.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace OnlineShop.Web; + +public static class AuthExtensions +{ + public static void ConfigureWebAppOpenIdConnect(this AuthenticationBuilder authentication) + { + // Named options + authentication.Services + .AddOptions( + OpenIdConnectDefaults.AuthenticationScheme) + .Configure< + IConfiguration, + IHttpClientFactory, + IHostEnvironment>(Configure); + + // Unnamed options + authentication.Services.AddOptions() + .Configure< + IConfiguration, + IHttpClientFactory, + IHostEnvironment>(Configure); + + static void Configure( + OpenIdConnectOptions options, + IConfiguration configuration, + IHttpClientFactory httpClientFactory, + IHostEnvironment hostEnvironment) + { + var backchannelHttpClient = + httpClientFactory.CreateClient( + "OidcBackchannel"); + + options.Backchannel = backchannelHttpClient; + options.Authority = + backchannelHttpClient + .GetIdpAuthorityUri().ToString(); + options.ClientId = "webapp"; + options.ClientSecret = + Environment + .GetEnvironmentVariable( + "Identity__ClientSecret"); + options.ResponseType = + OpenIdConnectResponseType.Code; + options.SaveTokens = true; + options.RequireHttpsMetadata = + !hostEnvironment.IsDevelopment(); + options.MapInboundClaims = false; + } + } + +} diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/OnlineShop.Web.csproj b/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/OnlineShop.Web.csproj index 6defd4b..f4700c2 100644 --- a/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/OnlineShop.Web.csproj +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/OnlineShop.Web.csproj @@ -6,6 +6,11 @@ enable + + + + + diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/Program.cs b/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/Program.cs index 3e6891c..feff627 100644 --- a/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/Program.cs +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.Web/Program.cs @@ -1,30 +1,45 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using OnlineShop.Web; using OnlineShop.Web.Components; var builder = WebApplication.CreateBuilder(args); -// Add service defaults & Aspire client integrations. +builder.AddRedisOutputCache("cache"); + builder.AddServiceDefaults(); -// Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); builder.Services.AddOutputCache(); +builder.Services.AddHttpClient( + "OidcBackchannel", o => o.BaseAddress = new("http://idp")); + builder.Services.AddHttpClient(client => { - // This URL uses "https+http://" to indicate HTTPS is preferred over HTTP. - // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes. client.BaseAddress = new("https+http://apiservice"); }); +builder.Services.AddAuthentication(options => +{ + options.DefaultScheme = + CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = + OpenIdConnectDefaults.AuthenticationScheme; +}) +.AddCookie( + CookieAuthenticationDefaults.AuthenticationScheme) +.AddOpenIdConnect() +.ConfigureWebAppOpenIdConnect(); + + var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } diff --git a/AppWithKeycloakAuth/OnlineShop/OnlineShop.slnx b/AppWithKeycloakAuth/OnlineShop/OnlineShop.slnx index fea54a9..34e56f1 100644 --- a/AppWithKeycloakAuth/OnlineShop/OnlineShop.slnx +++ b/AppWithKeycloakAuth/OnlineShop/OnlineShop.slnx @@ -1,6 +1,7 @@ +