yavsc/src/Yavsc.Org/Extensions/HostingExtensions.cs

803 lines
31 KiB
C#
Raw Normal View History

2024-02-25 18:05:10 +00:00
using System.Globalization;
2026-04-19 16:02:50 +01:00
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
2026-06-21 03:55:32 +01:00
using System.Security.Cryptography.X509Certificates;
2024-02-25 18:05:10 +00:00
using Google.Apis.Util.Store;
2026-04-19 16:02:50 +01:00
using IdentityModel;
2025-02-08 20:06:24 +00:00
using IdentityServer8;
2026-04-19 16:02:50 +01:00
using IdentityServer8.EntityFramework.Entities;
using IdentityServer8.EntityFramework.Services;
using IdentityServer8.EntityFramework.Stores;
2025-08-24 16:07:53 +01:00
using IdentityServer8.Stores;
2026-04-19 16:02:50 +01:00
using IdentityServer8.Validation;
2024-02-25 18:05:10 +00:00
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
2026-04-19 16:02:50 +01:00
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
2024-02-25 18:05:10 +00:00
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using Yavsc.Helpers;
using Yavsc.Interface;
2026-04-19 16:02:50 +01:00
using Yavsc.Interfaces;
2024-02-25 18:05:10 +00:00
using Yavsc.Models;
2026-04-19 16:02:50 +01:00
using Yavsc.Server.Helpers;
2024-02-25 18:05:10 +00:00
using Yavsc.Services;
2026-05-24 19:35:35 +01:00
using Yavsc.Services.Kyc;
2024-02-25 18:05:10 +00:00
using Yavsc.Settings;
2024-11-10 23:12:02 +00:00
using Yavsc.ViewModels.Auth;
2026-06-10 16:59:23 +01:00
using static IdentityServer8.IdentityServerConstants;
2024-02-25 18:05:10 +00:00
2024-11-06 13:00:34 +00:00
namespace Yavsc.Extensions;
2024-02-25 18:05:10 +00:00
2025-02-14 00:20:35 +00:00
public static class HostingExtensions
2024-02-25 18:05:10 +00:00
{
2026-05-28 22:18:26 +01:00
private const string InMemoryProviderName = "InMemory";
2025-08-18 09:22:09 +01:00
2025-07-14 18:58:04 +01:00
public static WebApplication ConfigureWebAppServices(this WebApplicationBuilder builder)
2024-02-25 18:05:10 +00:00
{
2026-02-07 23:47:12 +00:00
builder.Services.AddSwaggerGen();
2025-02-14 00:20:35 +00:00
IServiceCollection services = LoadConfiguration(builder);
2025-02-09 12:04:14 +00:00
2024-11-06 13:00:34 +00:00
services.AddSession();
2024-11-12 08:32:15 +00:00
2025-02-08 20:06:24 +00:00
// TODO .AddServerSideSessionStore<YavscServerSideSessionStore>()
2024-02-25 18:05:10 +00:00
2025-02-08 20:06:24 +00:00
// Add the system clock service
_ = services.AddSingleton<IConnexionManager, HubConnectionManager>();
_ = services.AddSingleton<ILiveProcessor, LiveProcessor>();
_ = services.AddTransient<IFileSystemAuthManager, FileSystemAuthManager>();
2025-07-31 11:44:02 +01:00
AddIdentityDBAndStores(builder)
.AddDefaultTokenProviders();
2025-02-16 22:40:51 +00:00
AddIdentityServer(builder);
2024-02-25 18:05:10 +00:00
2025-02-16 22:40:51 +00:00
services.AddSignalR(o =>
{
o.EnableDetailedErrors = true;
});
2025-08-18 09:22:09 +01:00
2025-02-08 20:06:24 +00:00
services.AddMvc(config =>
{
/* var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy)); */
config.Filters.Add(new ProducesAttribute("application/json"));
// config.ModelBinders.Insert(0,new MyDateTimeModelBinder());
// config.ModelBinders.Insert(0,new MyDecimalModelBinder());
config.EnableEndpointRouting = true;
}).AddFormatterMappings(
config => config.SetMediaTypeMappingForFormat("text/pdf",
new MediaTypeHeaderValue("text/pdf"))
).AddFormatterMappings(
config => config.SetMediaTypeMappingForFormat("text/x-tex",
new MediaTypeHeaderValue("text/x-tex"))
)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix,
options =>
{
options.ResourcesPath = "Resources";
}).AddDataAnnotationsLocalization();
services.AddTransient<ITrueEmailSender, MailSender>()
.AddTransient<Microsoft.AspNetCore.Identity.UI.Services.IEmailSender, MailSender>();
2026-04-20 00:35:51 +01:00
services.AddTransient<IYavscMessageSender, YavscMessageSender>()
2025-02-14 00:20:35 +00:00
.AddTransient<IBillingService, BillingService>()
.AddTransient<IDataStore, FileDataStore>((sp) => new FileDataStore("googledatastore", false))
2025-06-29 16:12:16 +01:00
.AddTransient<ICalendarManager, CalendarManager>()
2026-03-09 02:07:09 +00:00
.AddTransient<BlogSpotService>()
.AddTransient<ValidatingClientStore<ClientStore>>();
2024-02-25 18:05:10 +00:00
2025-02-08 20:06:24 +00:00
// TODO for SMS: services.AddTransient<ISmsSender, AuthMessageSender>();
2024-02-25 18:05:10 +00:00
2025-02-08 20:06:24 +00:00
_ = services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
2025-02-14 00:20:35 +00:00
var dataDirConfig = builder.Configuration["Site:DataDir"] ?? "DataDir";
var dataDir = new DirectoryInfo(dataDirConfig);
2025-02-08 20:06:24 +00:00
// Add session related services.
2024-02-25 18:05:10 +00:00
2025-02-08 20:06:24 +00:00
services.AddDataProtection().PersistKeysToFileSystem(dataDir);
Split Site:Audience into Site:ExternalUrl + Site:CorsAllowedOrigins The Site:Audience setting was conflating two distinct concepts: an OAuth JWT audience (a single resource identifier) and a CORS allow-list (an array of origins). Collapsing them caused several latent bugs: - OAuth/JWT validation expected a single string while CORS WithOrigins accepts an array. - Password-reset callback URLs and OAuth client RedirectUri/Origin were being built from what was meant to be an audience identifier, not a base URL. - Yavsc.Org's main CORS policy was hardcoded to '*', with no way to restrict it without code changes. Changes: - SiteSettings.Audience (string) replaced with CorsAllowedOrigins (IList<string>). - OAuth JWT Authority still reads Site:Authority; Audience now reads Site:ExternalUrl (Org only; Api/Blogs use ValidateAudience=false). - MailSender and AccountController build reset-callback URLs from Site:ExternalUrl. - ClientController uses Site:ExternalUrl for OAuth RedirectUri/Origin defaults on newly created clients. - Yavsc.Api and Yavsc.Blogs now read CORS origins from Site:CorsAllowedOrigins instead of hardcoded URLs. Add shared AddYavscCors / AddYavscJwtBearer extension methods in Yavsc.Server/Helpers/ServiceExtensions.cs to enforce a single configuration contract across all runtime services (Api, Blogs, Org). Fails closed when CorsAllowedOrigins is empty; fails fast at startup when Site:Authority is missing. Remove obsolete ConfigurationHelpers.GetAudience (no remaining callers). Local appsettings-*.json files (which carry deployment-specific values and are gitignored) must be updated to add Site:CorsAllowedOrigins.
2026-06-19 13:15:21 +01:00
AddYavscPolicies(services, builder.Configuration);
2025-02-14 00:20:35 +00:00
2025-02-23 20:23:23 +00:00
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
2025-07-10 15:19:28 +01:00
services.AddTransient<IExternalIdentityManager, ExternalIdentityManager>();
2025-02-14 00:20:35 +00:00
2026-02-17 21:50:17 +00:00
services.AddAuthentication("Bearer")
Split Site:Audience into Site:ExternalUrl + Site:CorsAllowedOrigins The Site:Audience setting was conflating two distinct concepts: an OAuth JWT audience (a single resource identifier) and a CORS allow-list (an array of origins). Collapsing them caused several latent bugs: - OAuth/JWT validation expected a single string while CORS WithOrigins accepts an array. - Password-reset callback URLs and OAuth client RedirectUri/Origin were being built from what was meant to be an audience identifier, not a base URL. - Yavsc.Org's main CORS policy was hardcoded to '*', with no way to restrict it without code changes. Changes: - SiteSettings.Audience (string) replaced with CorsAllowedOrigins (IList<string>). - OAuth JWT Authority still reads Site:Authority; Audience now reads Site:ExternalUrl (Org only; Api/Blogs use ValidateAudience=false). - MailSender and AccountController build reset-callback URLs from Site:ExternalUrl. - ClientController uses Site:ExternalUrl for OAuth RedirectUri/Origin defaults on newly created clients. - Yavsc.Api and Yavsc.Blogs now read CORS origins from Site:CorsAllowedOrigins instead of hardcoded URLs. Add shared AddYavscCors / AddYavscJwtBearer extension methods in Yavsc.Server/Helpers/ServiceExtensions.cs to enforce a single configuration contract across all runtime services (Api, Blogs, Org). Fails closed when CorsAllowedOrigins is empty; fails fast at startup when Site:Authority is missing. Remove obsolete ConfigurationHelpers.GetAudience (no remaining callers). Local appsettings-*.json files (which carry deployment-specific values and are gitignored) must be updated to add Site:CorsAllowedOrigins.
2026-06-19 13:15:21 +01:00
.AddYavscJwtBearer(builder.Configuration,
configure: o => o.Audience = builder.Configuration.GetSection("Site")["ExternalUrl"]);
2025-08-18 09:22:09 +01:00
2025-07-31 11:44:02 +01:00
services.AddTransient<RoleManager<IdentityRole>>();
services.AddTransient<IRoleStore<IdentityRole>, RoleStore<IdentityRole, ApplicationDbContext>>();
2026-05-24 19:35:35 +01:00
services.Configure<KycOptions>(builder.Configuration.GetSection("Kyc"));
services.AddScoped<ITrustTokenService, TrustTokenService>();
2025-02-14 00:20:35 +00:00
return builder.Build();
}
public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder)
{
IServiceCollection services = builder.Services;
2026-05-30 19:34:22 +01:00
var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName);
2026-04-20 00:35:51 +01:00
services.AddDbContext<ApplicationDbContext>(options =>
{
2026-05-28 22:18:26 +01:00
if (UsesInMemoryProvider(connectionString))
{
options.UseInMemoryDatabase(connectionString);
}
else
{
options.UseNpgsql(connectionString,
options => options.MigrationsAssembly(typeof(Program).Assembly));
}
2026-04-20 00:35:51 +01:00
});
2025-02-14 00:20:35 +00:00
2026-04-19 16:02:50 +01:00
var identityBuilder = services.AddIdentity<ApplicationUser, IdentityRole>(
2025-07-11 13:27:31 +01:00
options =>
{
2026-02-17 19:24:30 +00:00
options.SignIn.RequireConfirmedAccount = builder.Environment.IsEnvironment(
builder.Environment.EnvironmentName);
2025-07-31 11:44:02 +01:00
options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.PreferredUserName;
2026-05-30 19:34:22 +01:00
options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType;
2025-07-11 13:27:31 +01:00
}
)
2025-07-31 11:44:02 +01:00
.AddEntityFrameworkStores<ApplicationDbContext>();
2026-04-19 16:02:50 +01:00
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
// Dev-only: Chromium rejects SameSite=None without Secure on http://
// (e.g. http://localhost:5000). The default Identity cookie policy
// sets SameSite=None, which is invalid without Secure. Force Lax in
// dev. In production (https://) the default SameSite=None is fine.
if (builder.Environment.IsDevelopment())
{
services.ConfigureApplicationCookie(options =>
{
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
services.ConfigureExternalCookie(options =>
{
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
}
2026-04-19 16:02:50 +01:00
return identityBuilder;
2025-02-14 00:20:35 +00:00
}
Split Site:Audience into Site:ExternalUrl + Site:CorsAllowedOrigins The Site:Audience setting was conflating two distinct concepts: an OAuth JWT audience (a single resource identifier) and a CORS allow-list (an array of origins). Collapsing them caused several latent bugs: - OAuth/JWT validation expected a single string while CORS WithOrigins accepts an array. - Password-reset callback URLs and OAuth client RedirectUri/Origin were being built from what was meant to be an audience identifier, not a base URL. - Yavsc.Org's main CORS policy was hardcoded to '*', with no way to restrict it without code changes. Changes: - SiteSettings.Audience (string) replaced with CorsAllowedOrigins (IList<string>). - OAuth JWT Authority still reads Site:Authority; Audience now reads Site:ExternalUrl (Org only; Api/Blogs use ValidateAudience=false). - MailSender and AccountController build reset-callback URLs from Site:ExternalUrl. - ClientController uses Site:ExternalUrl for OAuth RedirectUri/Origin defaults on newly created clients. - Yavsc.Api and Yavsc.Blogs now read CORS origins from Site:CorsAllowedOrigins instead of hardcoded URLs. Add shared AddYavscCors / AddYavscJwtBearer extension methods in Yavsc.Server/Helpers/ServiceExtensions.cs to enforce a single configuration contract across all runtime services (Api, Blogs, Org). Fails closed when CorsAllowedOrigins is empty; fails fast at startup when Site:Authority is missing. Remove obsolete ConfigurationHelpers.GetAudience (no remaining callers). Local appsettings-*.json files (which carry deployment-specific values and are gitignored) must be updated to add Site:CorsAllowedOrigins.
2026-06-19 13:15:21 +01:00
private static void AddYavscPolicies(IServiceCollection services, IConfiguration configuration)
2025-02-14 00:20:35 +00:00
{
2025-02-08 20:06:24 +00:00
services.AddAuthorization(options =>
{
2025-02-08 21:58:23 +00:00
options.AddPolicy("ApiScope", policy =>
2025-02-11 04:45:05 +00:00
{
policy.RequireAuthenticatedUser()
.RequireClaim("scope", "scope2");
});
2025-08-25 13:32:31 +01:00
2025-02-09 16:57:10 +00:00
options.AddPolicy("Performer", policy =>
{
policy
.RequireAuthenticatedUser()
2026-05-30 19:34:22 +01:00
.RequireClaim(YavscConstants.RoleClaimType,
new string[] { YavscConstants.PerformerGroupName, YavscConstants.AdminGroupName })
2025-08-18 11:27:13 +01:00
;
2025-02-09 16:57:10 +00:00
});
2025-02-08 20:06:24 +00:00
options.AddPolicy("AdministratorOnly", policy =>
2024-02-25 18:05:10 +00:00
{
2025-07-07 07:49:18 +01:00
_ = policy
.RequireAuthenticatedUser()
2026-05-30 19:34:22 +01:00
.RequireClaim(YavscConstants.RoleClaimType, YavscConstants.AdminGroupName);
2024-02-25 18:05:10 +00:00
});
2026-05-30 19:34:22 +01:00
options.AddPolicy("FrontOffice", policy => policy.RequireRole(YavscConstants.FrontOfficeGroupName));
2025-02-14 00:20:35 +00:00
2025-02-08 20:06:24 +00:00
// options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456"));
// options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement()));
options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
2025-06-29 16:20:37 +01:00
options.AddPolicy("TheAuthor", policy => policy.Requirements.Add(new EditPermission()));
2025-02-08 20:06:24 +00:00
});
Split Site:Audience into Site:ExternalUrl + Site:CorsAllowedOrigins The Site:Audience setting was conflating two distinct concepts: an OAuth JWT audience (a single resource identifier) and a CORS allow-list (an array of origins). Collapsing them caused several latent bugs: - OAuth/JWT validation expected a single string while CORS WithOrigins accepts an array. - Password-reset callback URLs and OAuth client RedirectUri/Origin were being built from what was meant to be an audience identifier, not a base URL. - Yavsc.Org's main CORS policy was hardcoded to '*', with no way to restrict it without code changes. Changes: - SiteSettings.Audience (string) replaced with CorsAllowedOrigins (IList<string>). - OAuth JWT Authority still reads Site:Authority; Audience now reads Site:ExternalUrl (Org only; Api/Blogs use ValidateAudience=false). - MailSender and AccountController build reset-callback URLs from Site:ExternalUrl. - ClientController uses Site:ExternalUrl for OAuth RedirectUri/Origin defaults on newly created clients. - Yavsc.Api and Yavsc.Blogs now read CORS origins from Site:CorsAllowedOrigins instead of hardcoded URLs. Add shared AddYavscCors / AddYavscJwtBearer extension methods in Yavsc.Server/Helpers/ServiceExtensions.cs to enforce a single configuration contract across all runtime services (Api, Blogs, Org). Fails closed when CorsAllowedOrigins is empty; fails fast at startup when Site:Authority is missing. Remove obsolete ConfigurationHelpers.GetAudience (no remaining callers). Local appsettings-*.json files (which carry deployment-specific values and are gitignored) must be updated to add Site:CorsAllowedOrigins.
2026-06-19 13:15:21 +01:00
services.AddYavscCors(configuration);
2025-02-14 00:20:35 +00:00
}
2025-02-08 20:06:24 +00:00
2025-02-14 00:20:35 +00:00
public static IServiceCollection LoadConfiguration(this WebApplicationBuilder builder)
{
var siteSection = builder.Configuration.GetSection("Site");
2026-02-17 21:50:17 +00:00
2025-02-14 00:20:35 +00:00
var smtpSection = builder.Configuration.GetSection("Smtp");
var paypalSection = builder.Configuration.GetSection("Authentication:PayPal");
// OAuth2AppSettings
var googleAuthSettings = builder.Configuration.GetSection("Authentication:Google");
2026-02-17 19:24:30 +00:00
LoadGoogleConfig(builder.Configuration);
2025-02-14 00:20:35 +00:00
2025-08-18 09:22:09 +01:00
2025-02-14 00:20:35 +00:00
var services = builder.Services;
_ = services.AddControllersWithViews()
.AddNewtonsoftJson();
2025-08-18 09:22:09 +01:00
2025-02-14 00:20:35 +00:00
services.Configure<SiteSettings>(siteSection);
services.Configure<SmtpSettings>(smtpSection);
services.Configure<PayPalSettings>(paypalSection);
services.Configure<GoogleAuthSettings>(googleAuthSettings);
ConfigureRequestLocalization(services);
2024-11-10 23:12:02 +00:00
2025-02-14 00:20:35 +00:00
return services;
}
2025-02-11 04:45:05 +00:00
2025-02-16 22:40:51 +00:00
private static void AddAuthentication(WebApplicationBuilder builder)
2025-02-15 14:46:40 +00:00
{
2025-08-18 09:22:09 +01:00
IServiceCollection services = builder.Services;
IConfigurationRoot configurationRoot = builder.Configuration;
2025-02-16 22:40:51 +00:00
string? googleClientId = configurationRoot["Authentication:Google:ClientId"];
2025-02-15 14:46:40 +00:00
string? googleClientSecret = configurationRoot["Authentication:Google:ClientSecret"];
2025-08-18 09:22:09 +01:00
2025-02-16 22:40:51 +00:00
var authenticationBuilder = services.AddAuthentication();
2025-02-15 14:46:40 +00:00
2025-08-18 09:22:09 +01:00
if (googleClientId != null && googleClientSecret != null)
authenticationBuilder.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to https://localhost:5001/signin-google
options.ClientId = googleClientId;
options.ClientSecret = googleClientSecret;
});
2025-02-15 14:46:40 +00:00
}
2026-04-19 16:02:50 +01:00
public static IIdentityServerBuilder AddIdentityServer(WebApplicationBuilder builder)
2025-02-14 00:20:35 +00:00
{
2026-02-21 20:51:35 +00:00
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserIdClaimType = JwtClaimTypes.Subject;
options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.Name;
2026-05-30 19:34:22 +01:00
options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType;
2026-02-21 20:51:35 +00:00
});
2026-02-22 23:39:56 +00:00
var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name;
2026-05-30 19:34:22 +01:00
var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName);
2026-04-20 00:35:51 +01:00
string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}";
2025-08-24 16:07:53 +01:00
2025-02-16 22:40:51 +00:00
var identityServerBuilder = builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://IdentityServer8.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
2025-08-18 09:22:09 +01:00
2025-02-16 22:40:51 +00:00
})
2026-02-09 01:03:33 +00:00
.AddAspNetIdentity<ApplicationUser>()
.AddClientStore<ClientStore>()
2026-03-09 02:07:09 +00:00
.AddClientConfigurationValidator<DefaultClientConfigurationValidator>()
2026-02-09 01:03:33 +00:00
.AddCorsPolicyService<CorsPolicyService>()
.AddResourceStore<ResourceStore>()
2025-08-24 16:07:53 +01:00
.AddConfigurationStore(options =>
{
2026-05-28 22:18:26 +01:00
options.ConfigureDbContext = b =>
{
if (UsesInMemoryProvider(connectionString))
{
b.UseInMemoryDatabase(connectionString);
}
else
{
b.UseNpgsql(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
}
b.UseSeeding(EnsureDefaultConfiguration(builder.Configuration));
2026-05-28 22:18:26 +01:00
};
2025-08-24 16:07:53 +01:00
})
.AddOperationalStore(options =>
{
2026-05-28 22:18:26 +01:00
options.ConfigureDbContext = b =>
{
if (UsesInMemoryProvider(connectionString))
{
b.UseInMemoryDatabase(connectionString);
}
else
{
b.UseNpgsql(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
}
};
2026-04-20 00:35:51 +01:00
2026-02-09 01:03:33 +00:00
});
2025-08-18 09:22:09 +01:00
2025-02-14 00:20:35 +00:00
if (builder.Environment.IsDevelopment())
{
identityServerBuilder.AddDeveloperSigningCredential();
}
2026-06-21 03:55:32 +01:00
else
{
// Production: reuse the Let's Encrypt certificate that Kestrel
// already loads for TLS so IdentityServer has a stable signing
// key (and a JWKS endpoint). The cert is renewed by the ACME
// hook and a service restart picks up the new key automatically.
//
// The path comes from Kestrel:Endpoints:Https:Certificate to
// avoid maintaining a separate setting; fullchain.pem bundles
// the leaf + chain, which X509Certificate2 needs for chain
// validation by relying parties.
var certPath = builder.Configuration["Kestrel:Endpoints:Https:Certificate:Path"];
var keyPath = builder.Configuration["Kestrel:Endpoints:Https:Certificate:KeyPath"];
if (string.IsNullOrWhiteSpace(certPath) || string.IsNullOrWhiteSpace(keyPath))
{
throw new InvalidOperationException(
"Production IdentityServer requires a signing certificate. " +
"Configure Kestrel:Endpoints:Https:Certificate:{Path,KeyPath}.");
}
// CreateFromPemFile loads the leaf cert + its private key from
// PEM files without writing to the Windows certificate store
// (irrelevant on Linux, but keeps the call cross-platform).
var signingCert = X509Certificate2.CreateFromPemFile(certPath, keyPath);
// Pick the JWT signing algorithm from the cert's key type. Let's
// Encrypt may issue either RSA or ECDSA certificates depending on
// the ACME account's preferred chain; IdentityServer would 500 if
// we forced RS256 against an ECDSA key.
var algorithm = signingCert.GetECDsaPrivateKey() is not null ? "ES256" : "RS256";
identityServerBuilder.AddSigningCredential(signingCert, algorithm);
}
// Override the advertised jwks_uri to the canonical
// /.well-known/jwks endpoint that UseIdentityServer() actually
// mounts. IdentityServer8's default convention here is
// /.well-known/openid-configuration/jwks, which is not what most
// OIDC clients (including IdentityModel.OidcClient) expect, and
// would otherwise need a parallel route to be wired up.
identityServerBuilder.Services.Configure<IdentityServer8.Configuration.IdentityServerOptions>(options =>
{
options.Discovery.CustomEntries["jwks_uri"] = "/.well-known/jwks";
});
2025-02-14 00:20:35 +00:00
return identityServerBuilder;
2024-02-25 18:05:10 +00:00
}
2025-02-14 00:20:35 +00:00
2026-05-28 22:18:26 +01:00
private static bool UsesInMemoryProvider(string connectionString)
{
return string.Equals(connectionString, InMemoryProviderName, StringComparison.OrdinalIgnoreCase);
}
2026-04-20 00:35:51 +01:00
private static Action<DbContext, bool> EnsureDefaultApplicationScopes()
{
return (context, _) =>
{
2026-06-06 21:30:41 +01:00
foreach (String scope in Constants.BuildInApiScopes)
2026-04-20 00:35:51 +01:00
{
var existentScope = context.Set<ApiScope>().FirstOrDefault(b => b.Name == scope);
if (existentScope == null)
{
context.Set<ApiScope>().Add(new ApiScope { Name = scope });
context.SaveChanges();
}
}
2026-06-10 11:10:15 +01:00
};
}
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
private const string PostItClientId = "postit";
private static readonly string[] PostItRedirectUris = new[]
{
// Loopback URI for desktop / browser-based PKCE flows.
"http://127.0.0.1:7890/",
// Custom-scheme URI for Android. The matching IntentFilter must be
// declared in PostIt.Android/Properties/AndroidManifest.xml.
"android://postit-signin",
};
private static readonly string[] PostItGrantTypes = new[]
{
"authorization_code",
"client_credentials",
};
private static readonly string[] PostItScopes = new[]
{
"blog",
IdentityServer8.IdentityServerConstants.StandardScopes.OpenId,
IdentityServer8.IdentityServerConstants.StandardScopes.Profile,
};
private static Action<DbContext, bool> EnsureDefaultConfiguration(
IConfiguration configuration
)
2026-06-10 11:10:15 +01:00
{
return (context, _) =>
{
EnsureDefaultApplicationScopes()(context, _);
2026-04-20 00:35:51 +01:00
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
var clients = context.Set<IdentityServer8.EntityFramework.Entities.Client>();
var existingClient = clients.FirstOrDefault(c => c.ClientId == PostItClientId);
if (existingClient is null)
2026-06-10 11:10:15 +01:00
{
SeedNewPostItClient(configuration, context);
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
return;
}
2026-06-10 11:10:15 +01:00
MigratePostItClientToPublic(configuration, context, existingClient);
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
};
}
/// <summary>
/// Insert a brand new <c>postit</c> client configured as a public OIDC
/// client using Authorization Code + PKCE. Used the first time the
/// ConfigurationDb is seeded.
/// </summary>
private static void SeedNewPostItClient(IConfiguration configuration, DbContext context)
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
{
// PostIt is a public client (Authorization Code + PKCE).
// No client secret is stored or transmitted; PKCE binds the
// authorization code to the requesting device.
var client = new IdentityServer8.EntityFramework.Entities.Client
{
ClientId = PostItClientId,
Enabled = true,
RequireClientSecret = false,
RequirePkce = true,
ProtocolType = "oidc",
RequireConsent = false,
};
context.Set<Client>().Add(client);
foreach (var grantType in PostItGrantTypes)
{
context.Set<ClientGrantType>().Add(new IdentityServer8.EntityFramework.Entities.ClientGrantType
{
Client = client,
GrantType = grantType
});
}
foreach (var scope in PostItScopes)
{
context.Set<ClientScope>().Add(new IdentityServer8.EntityFramework.Entities.ClientScope
{
Client = client,
Scope = scope
});
}
foreach (var redirectUri in BuildPostItRedirectUris(configuration))
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
{
context.Set<ClientRedirectUri>().Add(new IdentityServer8.EntityFramework.Entities.ClientRedirectUri
{
Client = client,
RedirectUri = redirectUri
});
}
// No ClientSecret row: PKCE-only clients don't need one.
context.SaveChanges();
}
/// <summary>
/// Compose the full set of redirect URIs for the PostIt client. The base
/// URIs cover the standalone desktop/mobile flows; the value of
/// <c>Site:ExternalUrl</c> is appended so PostIt can also be embedded in
/// a Yavsc.Org web page (e.g. an iframe-launched launcher).
/// </summary>
private static IEnumerable<string> BuildPostItRedirectUris(IConfiguration configuration)
{
foreach (var uri in PostItRedirectUris)
yield return uri;
var externalUrl = configuration["Site:ExternalUrl"];
if (!string.IsNullOrWhiteSpace(externalUrl))
yield return externalUrl;
}
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
/// <summary>
/// Bring an existing <c>postit</c> client up to the current public-client
/// configuration. Idempotent: each change is applied only when the row is
/// currently in the legacy state.
/// </summary>
private static void MigratePostItClientToPublic(
IConfiguration configuration,
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
DbContext context,
IdentityServer8.EntityFramework.Entities.Client client)
{
var changed = false;
// 1. Drop the client secret. PKCE-only clients must not have one.
var secrets = context.Set<ClientSecret>().Where(s => s.Client.Id == client.Id);
if (secrets.Any())
{
context.Set<ClientSecret>().RemoveRange(secrets);
changed = true;
}
// 2. Flip the security flags.
if (client.RequireClientSecret)
{
client.RequireClientSecret = false;
changed = true;
}
if (!client.RequirePkce)
{
client.RequirePkce = true;
changed = true;
}
// 3. Ensure all expected grant types are present (don't remove extras
// that may have been added by hand).
var existingGrantTypes = context.Set<ClientGrantType>()
.Where(g => g.Client.Id == client.Id)
.Select(g => g.GrantType)
.ToHashSet();
foreach (var grantType in PostItGrantTypes)
{
if (!existingGrantTypes.Contains(grantType))
{
2026-06-10 16:59:23 +01:00
context.Set<ClientGrantType>().Add(new IdentityServer8.EntityFramework.Entities.ClientGrantType
2026-06-10 11:10:15 +01:00
{
Client = client,
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
GrantType = grantType
2026-06-10 11:10:15 +01:00
});
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
changed = true;
}
}
2026-06-10 16:59:23 +01:00
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
// 4. Ensure all expected scopes are present.
var existingScopes = context.Set<ClientScope>()
.Where(s => s.Client.Id == client.Id)
.Select(s => s.Scope)
.ToHashSet();
foreach (var scope in PostItScopes)
{
if (!existingScopes.Contains(scope))
{
2026-06-10 16:59:23 +01:00
context.Set<ClientScope>().Add(new IdentityServer8.EntityFramework.Entities.ClientScope
2026-06-10 11:10:15 +01:00
{
Client = client,
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
Scope = scope
2026-06-10 16:59:23 +01:00
});
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
changed = true;
}
}
2026-06-10 16:59:23 +01:00
// 5. Ensure all expected redirect URIs are present. The expected set
// is built by BuildPostItRedirectUris: the standalone URIs from
// PostItRedirectUris (desktop loopback + Android custom scheme)
// plus Site:ExternalUrl so PostIt can be embedded in a Yavsc.Org
// web page. Any pre-existing rows that are no longer in this set
// are removed.
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
var existingRedirects = context.Set<ClientRedirectUri>()
.Where(r => r.Client.Id == client.Id)
.ToList();
var existingRedirectUris = existingRedirects
.Select(r => r.RedirectUri)
.ToHashSet(StringComparer.Ordinal);
foreach (var redirectUri in BuildPostItRedirectUris(configuration))
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
{
if (!existingRedirectUris.Contains(redirectUri))
{
2026-06-10 16:59:23 +01:00
context.Set<ClientRedirectUri>().Add(new IdentityServer8.EntityFramework.Entities.ClientRedirectUri
{
Client = client,
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
RedirectUri = redirectUri
2026-06-10 16:59:23 +01:00
});
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
changed = true;
}
}
2026-06-10 16:59:23 +01:00
PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT) PostIt is a desktop/mobile app talking to Yavsc.Org (https://yavsc.pschneider.fr) as an OIDC identity provider. The previous grant used the client_credentials flow with a client_secret embedded in postit-settings.json: this was both insecure (secret travels with the binary) and inappropriate for an interactive app (token had no user identity, so the API could not scope or audit). The new flow is Authorization Code + PKCE: * PostIt client (Settings/AuthenticationSettings.cs): the ClientSecret property is removed; GetOidcClientOptions now drops the secret and accepts an optional IBrowser supplied per-platform. * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin') that the Android app uses; RedirectUri is no longer hard-coded in MainViewModel. * MainViewModel.cs: the manual discovery + client_credentials POST is replaced with OidcClient.LoginAsync (Authorization Code + PKCE). * Settings sample: Authority points at the real Yavsc.Org OP, not at a non-existent Keycloak-style realm path. * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed is now idempotent (MigratePostItClientToPublic) and detects legacy state on existing ConfigurationDb rows - flips RequireClientSecret=false, RequirePkce=true, drops any ClientSecret row, and replaces the legacy RedirectUris (https://yavsc.pschneider.fr/, yavsc://callback) with the current set (http://127.0.0.1:7890/, android://postit-signin). PostIt.Android: * MainActivity: explicit Name attribute so the activity alias can target a stable component; LaunchMode.SingleTask so the existing instance receives the deep-link Intent; OnNewIntent forwards the callback URI through AndroidOidcCallbackSink. * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity exposing scheme=android host=postit-signin to Android, so the OP redirect lands back in the running PostIt instance. The IdentityModel.OidcClient.Browser.SystemBrowser package and a thin AndroidSystemBrowser implementation are added in a follow-up so OidcClient.LoginAsync can actually drive Chrome Custom Tabs and consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
if (changed)
{
context.SaveChanges();
}
2026-04-20 00:35:51 +01:00
}
2025-02-14 00:20:35 +00:00
private static void ConfigureRequestLocalization(IServiceCollection services)
{
services.Configure<RequestLocalizationOptions>(options =>
{
CultureInfo[] supportedCultures = new[]
{
2026-02-17 19:24:30 +00:00
new CultureInfo("en"),
new CultureInfo("fr"),
new CultureInfo("pt")
2025-02-14 00:20:35 +00:00
};
CultureInfo[] supportedUICultures = new[]
{
2026-02-17 19:24:30 +00:00
new CultureInfo("fr"),
new CultureInfo("en"),
new CultureInfo("pt")
2025-02-14 00:20:35 +00:00
};
// You must explicitly state which cultures your application supports.
// These are the cultures the app supports for formatting numbers, dates, etc.
options.SupportedCultures = supportedCultures;
// These are the cultures the app supports for UI strings, i.e. we have localized resources for.
options.SupportedUICultures = supportedUICultures;
options.RequestCultureProviders = new List<IRequestCultureProvider>
2026-02-17 19:24:30 +00:00
{
new QueryStringRequestCultureProvider { Options = options },
new CookieRequestCultureProvider { Options = options, CookieName="ASPNET_CULTURE" },
new AcceptLanguageHeaderRequestCultureProvider { Options = options }
};
2025-02-14 00:20:35 +00:00
});
}
2025-07-14 18:58:04 +01:00
public async static Task<WebApplication> ConfigurePipeline(this WebApplication app)
2025-02-08 20:06:24 +00:00
{
ILoggerFactory loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<Program>();
2025-02-08 20:06:24 +00:00
2025-09-14 00:41:35 +01:00
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
2025-09-14 23:52:21 +01:00
JwtSecurityTokenHandler.DefaultMapInboundClaims = true;
2024-02-25 18:05:10 +00:00
if (app.Environment.IsDevelopment())
{
2026-02-07 23:47:12 +00:00
app.UseDeveloperExceptionPage();
2024-02-25 18:05:10 +00:00
}
2025-02-08 20:06:24 +00:00
else
2024-11-12 08:32:15 +00:00
{
app.UseExceptionHandler("/Home/Error");
2026-03-01 20:14:54 +00:00
app.MigrateDatabase();
2024-11-12 08:32:15 +00:00
}
2025-08-18 09:22:09 +01:00
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/robots.txt"))
{
2025-07-10 18:32:58 +01:00
var robotsTxtPath = System.IO.Path.Combine(app.Environment.WebRootPath, $"robots.txt");
string output = "User-agent: * \nDisallow: /";
2025-08-18 09:22:09 +01:00
if (File.Exists(robotsTxtPath))
{
2025-07-10 18:32:58 +01:00
output = await File.ReadAllTextAsync(robotsTxtPath);
}
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(output);
2025-08-18 09:22:09 +01:00
}
else await next();
2025-07-10 18:32:58 +01:00
});
2026-02-07 23:47:12 +00:00
2026-02-09 01:03:33 +00:00
app.UseSwagger();
2026-02-07 23:47:12 +00:00
app.UseSwaggerUI();
2024-02-25 18:05:10 +00:00
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
2025-02-12 20:41:14 +00:00
app.UseCors("default");
2026-06-15 02:55:22 +01:00
app.MapStaticAssets();
2025-02-16 22:40:51 +00:00
app.MapDefaultControllerRoute();
2025-07-07 07:49:18 +01:00
//app.MapRazorPages();
2024-03-04 01:02:19 +00:00
app.MapHub<ChatHub>("/chatHub");
2025-08-18 09:22:09 +01:00
2025-02-26 18:59:08 +00:00
WorkflowHelpers.ConfigureBillingService();
2025-08-18 09:22:09 +01:00
2024-02-25 18:05:10 +00:00
var services = app.Services;
var siteSettings = services.GetRequiredService<IOptions<SiteSettings>>();
var smtpSettings = services.GetRequiredService<IOptions<SmtpSettings>>();
var payPalSettings = services.GetRequiredService<IOptions<PayPalSettings>>();
var googleAuthSettings = services.GetRequiredService<IOptions<GoogleAuthSettings>>();
2025-08-31 18:27:53 +01:00
var localization = services.GetRequiredService<IStringLocalizer<Startup>>();
2025-02-23 20:23:23 +00:00
Startup.Configure(app, siteSettings, smtpSettings,
2024-02-25 18:05:10 +00:00
payPalSettings, googleAuthSettings, localization, loggerFactory,
2025-02-08 20:06:24 +00:00
app.Environment.EnvironmentName);
2024-02-25 18:05:10 +00:00
app.ConfigureFileServerApp();
2026-02-09 01:03:33 +00:00
app.UseSession();
2024-02-25 18:05:10 +00:00
return app;
}
2026-03-16 23:16:12 +00:00
2026-03-01 20:14:54 +00:00
private static void MigrateDatabase(this IApplicationBuilder app)
2025-08-24 16:07:53 +01:00
{
2026-02-07 23:47:12 +00:00
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
2025-08-24 16:07:53 +01:00
{
2025-08-25 13:32:31 +01:00
2025-08-25 14:18:26 +01:00
try
2025-08-24 16:07:53 +01:00
{
2026-02-28 14:22:33 +00:00
foreach (Type contextType in new Type[]
{
typeof(ApplicationDbContext)
})
{
((DbContext)serviceScope.ServiceProvider
.GetRequiredService(contextType))
.Database.Migrate();
}
2025-09-14 23:52:21 +01:00
}
2025-08-25 14:18:26 +01:00
catch (InvalidOperationException ex)
{
app.Properties["DegradedDBContext"] = ex.Message;
2025-08-24 16:07:53 +01:00
}
}
}
2024-02-25 18:05:10 +00:00
2025-02-08 20:06:24 +00:00
static void LoadGoogleConfig(IConfigurationRoot configuration)
2024-02-25 18:05:10 +00:00
{
string? googleClientFile = configuration["Authentication:Google:GoogleWebClientJson"];
string? googleServiceAccountJsonFile = configuration["Authentication:Google:GoogleServiceAccountJson"];
if (googleClientFile != null)
{
Config.GoogleWebClientConfiguration = new ConfigurationBuilder().AddJsonFile(googleClientFile).Build();
}
if (googleServiceAccountJsonFile != null)
{
FileInfo safile = new FileInfo(googleServiceAccountJsonFile);
Config.GServiceAccount = JsonConvert.DeserializeObject<GoogleServiceAccount>(safile.OpenText().ReadToEnd());
}
}
2025-08-18 09:22:09 +01:00
2025-07-10 09:16:58 +01:00
public static IApplicationBuilder ConfigureFileServerApp(this IApplicationBuilder app,
bool enableDirectoryBrowsing = false)
2025-07-07 07:49:18 +01:00
{
2025-07-10 09:16:58 +01:00
var userFilesDirInfo = new DirectoryInfo(Config.SiteSetup.Blog);
AbstractFileSystemHelpers.UserFilesDirName = userFilesDirInfo.FullName;
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
if (!userFilesDirInfo.Exists) userFilesDirInfo.Create();
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
Config.UserFilesOptions = new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(AbstractFileSystemHelpers.UserFilesDirName),
2026-05-30 19:34:22 +01:00
RequestPath = PathString.FromUriComponent(YavscConstants.UserFilesPath),
2025-07-10 09:16:58 +01:00
EnableDirectoryBrowsing = enableDirectoryBrowsing,
};
Config.UserFilesOptions.EnableDefaultFiles = true;
Config.UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes = true;
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
var avatarsDirInfo = new DirectoryInfo(Config.SiteSetup.Avatars);
if (!avatarsDirInfo.Exists) avatarsDirInfo.Create();
Config.AvatarsDirName = avatarsDirInfo.FullName;
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
Config.AvatarsOptions = new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(Config.AvatarsDirName),
2026-05-30 19:34:22 +01:00
RequestPath = PathString.FromUriComponent(YavscConstants.AvatarsPath),
2025-07-10 09:16:58 +01:00
EnableDirectoryBrowsing = enableDirectoryBrowsing
};
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
var gitdirinfo = new DirectoryInfo(Config.SiteSetup.GitRepository);
Config.GitDirName = gitdirinfo.FullName;
if (!gitdirinfo.Exists) gitdirinfo.Create();
Config.GitOptions = new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(Config.GitDirName),
2026-05-30 19:34:22 +01:00
RequestPath = PathString.FromUriComponent(YavscConstants.GitPath),
2025-07-10 09:16:58 +01:00
EnableDirectoryBrowsing = enableDirectoryBrowsing,
};
Config.GitOptions.DefaultFilesOptions.DefaultFileNames.Add("index.md");
Config.GitOptions.StaticFileOptions.ServeUnknownFileTypes = true;
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
app.UseFileServer(Config.UserFilesOptions);
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
app.UseFileServer(Config.AvatarsOptions);
2025-07-07 07:49:18 +01:00
2025-07-10 09:16:58 +01:00
app.UseFileServer(Config.GitOptions);
app.UseStaticFiles();
return app;
2025-07-07 07:49:18 +01:00
}
}