using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Reflection; using Google.Apis.Util.Store; using IdentityModel; using IdentityServer8; using IdentityServer8.EntityFramework.Entities; using IdentityServer8.EntityFramework.Services; using IdentityServer8.EntityFramework.Stores; using IdentityServer8.Stores; using IdentityServer8.Validation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 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; using Yavsc.Interfaces; using Yavsc.Models; using Yavsc.Server.Helpers; using Yavsc.Services; using Yavsc.Services.Kyc; using Yavsc.Settings; using Yavsc.ViewModels.Auth; namespace Yavsc.Extensions; public static class HostingExtensions { private const string InMemoryProviderName = "InMemory"; public static WebApplication ConfigureWebAppServices(this WebApplicationBuilder builder) { builder.Services.AddSwaggerGen(); IServiceCollection services = LoadConfiguration(builder); services.AddSession(); // TODO .AddServerSideSessionStore() // Add the system clock service _ = services.AddSingleton(); _ = services.AddSingleton(); _ = services.AddTransient(); AddIdentityDBAndStores(builder) .AddDefaultTokenProviders(); AddIdentityServer(builder); services.AddSignalR(o => { o.EnableDetailedErrors = true; }); 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() .AddTransient(); services.AddTransient() .AddTransient() .AddTransient((sp) => new FileDataStore("googledatastore", false)) .AddTransient() .AddTransient() .AddTransient>(); // TODO for SMS: services.AddTransient(); _ = services.AddLocalization(options => { options.ResourcesPath = "Resources"; }); var dataDirConfig = builder.Configuration["Site:DataDir"] ?? "DataDir"; var dataDir = new DirectoryInfo(dataDirConfig); // Add session related services. services.AddDataProtection().PersistKeysToFileSystem(dataDir); AddYavscPolicies(services); services.AddScoped(); services.AddTransient(); services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.IncludeErrorDetails = true; options.Authority = builder.Configuration.GetSection("Site")["Authority"]; options.Audience = builder.Configuration.GetSection("Site")["Audience"]; options.TokenValidationParameters = new() { ValidateAudience = false, RoleClaimType = Constants.RoleClaimType }; options.MapInboundClaims = true; }); services.AddTransient>(); services.AddTransient, RoleStore>(); services.Configure(builder.Configuration.GetSection("Kyc")); services.AddScoped(); return builder.Build(); } public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder) { IServiceCollection services = builder.Services; var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); services.AddDbContext(options => { if (UsesInMemoryProvider(connectionString)) { options.UseInMemoryDatabase(connectionString); } else { options.UseNpgsql(connectionString, options => options.MigrationsAssembly(typeof(Program).Assembly)); } }); var identityBuilder = services.AddIdentity( options => { options.SignIn.RequireConfirmedAccount = builder.Environment.IsEnvironment( builder.Environment.EnvironmentName); options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.PreferredUserName; options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType; } ) .AddEntityFrameworkStores(); services.AddScoped, UserClaimsPrincipalFactory>(); return identityBuilder; } private static void AddYavscPolicies(IServiceCollection services) { services.AddAuthorization(options => { options.AddPolicy("ApiScope", policy => { policy.RequireAuthenticatedUser() .RequireClaim("scope", "scope2"); }); options.AddPolicy("Performer", policy => { policy .RequireAuthenticatedUser() .RequireClaim(Constants.RoleClaimType, new string[] { Constants.PerformerGroupName, Constants.AdminGroupName }) ; }); options.AddPolicy("AdministratorOnly", policy => { _ = policy .RequireAuthenticatedUser() .RequireClaim(Constants.RoleClaimType, Constants.AdminGroupName); }); options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName)); // options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456")); // options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement())); options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser()); options.AddPolicy("TheAuthor", policy => policy.Requirements.Add(new EditPermission())); }) .AddCors(options => { options.AddPolicy("default", builder => { _ = builder.WithOrigins("*") .AllowAnyHeader() .AllowAnyMethod(); }); }); } public static IServiceCollection LoadConfiguration(this WebApplicationBuilder builder) { var siteSection = builder.Configuration.GetSection("Site"); var smtpSection = builder.Configuration.GetSection("Smtp"); var paypalSection = builder.Configuration.GetSection("Authentication:PayPal"); // OAuth2AppSettings var googleAuthSettings = builder.Configuration.GetSection("Authentication:Google"); LoadGoogleConfig(builder.Configuration); var services = builder.Services; _ = services.AddControllersWithViews() .AddNewtonsoftJson(); services.Configure(siteSection); services.Configure(smtpSection); services.Configure(paypalSection); services.Configure(googleAuthSettings); ConfigureRequestLocalization(services); return services; } private static void AddAuthentication(WebApplicationBuilder builder) { IServiceCollection services = builder.Services; IConfigurationRoot configurationRoot = builder.Configuration; string? googleClientId = configurationRoot["Authentication:Google:ClientId"]; string? googleClientSecret = configurationRoot["Authentication:Google:ClientSecret"]; var authenticationBuilder = services.AddAuthentication(); 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; }); } public static IIdentityServerBuilder AddIdentityServer(WebApplicationBuilder builder) { builder.Services.Configure(options => { options.ClaimsIdentity.UserIdClaimType = JwtClaimTypes.Subject; options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.Name; options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType; }); var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}"; 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; }) .AddAspNetIdentity() .AddClientStore() .AddClientConfigurationValidator() .AddCorsPolicyService() .AddResourceStore() .AddConfigurationStore(options => { options.ConfigureDbContext = b => { if (UsesInMemoryProvider(connectionString)) { b.UseInMemoryDatabase(connectionString); } else { b.UseNpgsql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); } b.UseSeeding(EnsureDefaultApplicationScopes()); }; }) .AddOperationalStore(options => { options.ConfigureDbContext = b => { if (UsesInMemoryProvider(connectionString)) { b.UseInMemoryDatabase(connectionString); } else { b.UseNpgsql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); } }; }); if (builder.Environment.IsDevelopment()) { identityServerBuilder.AddDeveloperSigningCredential(); } return identityServerBuilder; } private static bool UsesInMemoryProvider(string connectionString) { return string.Equals(connectionString, InMemoryProviderName, StringComparison.OrdinalIgnoreCase); } private static Action EnsureDefaultApplicationScopes() { return (context, _) => { foreach (String scope in new string[] { "blog", "admin", "contract", "com" }) { var existentScope = context.Set().FirstOrDefault(b => b.Name == scope); if (existentScope == null) { context.Set().Add(new ApiScope { Name = scope }); context.SaveChanges(); } } }; } private static void ConfigureRequestLocalization(IServiceCollection services) { services.Configure(options => { CultureInfo[] supportedCultures = new[] { new CultureInfo("en"), new CultureInfo("fr"), new CultureInfo("pt") }; CultureInfo[] supportedUICultures = new[] { new CultureInfo("fr"), new CultureInfo("en"), new CultureInfo("pt") }; // 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 { new QueryStringRequestCultureProvider { Options = options }, new CookieRequestCultureProvider { Options = options, CookieName="ASPNET_CULTURE" }, new AcceptLanguageHeaderRequestCultureProvider { Options = options } }; }); } public async static Task ConfigurePipeline(this WebApplication app) { ILoggerFactory loggerFactory = app.Services.GetRequiredService(); var logger = loggerFactory.CreateLogger(); AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); JwtSecurityTokenHandler.DefaultMapInboundClaims = true; if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.MigrateDatabase(); } app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/robots.txt")) { var robotsTxtPath = System.IO.Path.Combine(app.Environment.WebRootPath, $"robots.txt"); string output = "User-agent: * \nDisallow: /"; if (File.Exists(robotsTxtPath)) { output = await File.ReadAllTextAsync(robotsTxtPath); } context.Response.ContentType = "text/plain"; await context.Response.WriteAsync(output); } else await next(); }); app.UseSwagger(); app.UseSwaggerUI(); app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.UseCors("default"); app.MapDefaultControllerRoute(); //app.MapRazorPages(); app.MapHub("/chatHub"); WorkflowHelpers.ConfigureBillingService(); var services = app.Services; var siteSettings = services.GetRequiredService>(); var smtpSettings = services.GetRequiredService>(); var payPalSettings = services.GetRequiredService>(); var googleAuthSettings = services.GetRequiredService>(); var localization = services.GetRequiredService>(); Startup.Configure(app, siteSettings, smtpSettings, payPalSettings, googleAuthSettings, localization, loggerFactory, app.Environment.EnvironmentName); app.ConfigureFileServerApp(); app.UseSession(); return app; } private static void MigrateDatabase(this IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) { try { foreach (Type contextType in new Type[] { typeof(ApplicationDbContext) }) { ((DbContext)serviceScope.ServiceProvider .GetRequiredService(contextType)) .Database.Migrate(); } } catch (InvalidOperationException ex) { app.Properties["DegradedDBContext"] = ex.Message; } } } static void LoadGoogleConfig(IConfigurationRoot configuration) { 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(safile.OpenText().ReadToEnd()); } } public static IApplicationBuilder ConfigureFileServerApp(this IApplicationBuilder app, bool enableDirectoryBrowsing = false) { var userFilesDirInfo = new DirectoryInfo(Config.SiteSetup.Blog); AbstractFileSystemHelpers.UserFilesDirName = userFilesDirInfo.FullName; if (!userFilesDirInfo.Exists) userFilesDirInfo.Create(); Config.UserFilesOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(AbstractFileSystemHelpers.UserFilesDirName), RequestPath = PathString.FromUriComponent(Constants.UserFilesPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.UserFilesOptions.EnableDefaultFiles = true; Config.UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes = true; var avatarsDirInfo = new DirectoryInfo(Config.SiteSetup.Avatars); if (!avatarsDirInfo.Exists) avatarsDirInfo.Create(); Config.AvatarsDirName = avatarsDirInfo.FullName; Config.AvatarsOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(Config.AvatarsDirName), RequestPath = PathString.FromUriComponent(Constants.AvatarsPath), EnableDirectoryBrowsing = enableDirectoryBrowsing }; 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), RequestPath = PathString.FromUriComponent(Constants.GitPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.GitOptions.DefaultFilesOptions.DefaultFileNames.Add("index.md"); Config.GitOptions.StaticFileOptions.ServeUnknownFileTypes = true; app.UseFileServer(Config.UserFilesOptions); app.UseFileServer(Config.AvatarsOptions); app.UseFileServer(Config.GitOptions); app.UseStaticFiles(); return app; } }