using System; using System.Collections.Generic; using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Interfaces; using Oqtane.Managers; using Oqtane.Modules; using Oqtane.Providers; using Oqtane.Repository; using Oqtane.Security; using Oqtane.Services; using Oqtane.Shared; namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { public static IServiceCollection AddOqtane(this IServiceCollection services, IConfigurationRoot configuration, IWebHostEnvironment environment) { // process forwarded headers on load balancers and proxy servers services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); // register localization services services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddOptions>().Bind(configuration.GetSection(SettingKeys.AvailableDatabasesSection)); // register scoped core services services.AddScoped() .AddOqtaneServerScopedServices(); services.AddSingleton(); // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) services.AddHttpClients(); // register singleton scoped core services services.AddSingleton(configuration) .AddOqtaneSingletonServices(); // install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain ) InstallationManager.InstallPackages(environment.WebRootPath, environment.ContentRootPath); // register transient scoped core services services.AddOqtaneTransientServices(); // load the external assemblies into the app domain, install services services.AddOqtaneAssemblies(); services.AddOqtaneDbContext(); services.AddAntiforgery(options => { options.HeaderName = Constants.AntiForgeryTokenHeaderName; options.Cookie.Name = Constants.AntiForgeryTokenCookieName; options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.HttpOnly = true; }); services.AddIdentityCore(options => { }) .AddEntityFrameworkStores() .AddSignInManager() .AddDefaultTokenProviders() .AddClaimsPrincipalFactory>(); // role claims services.ConfigureOqtaneIdentityOptions(configuration); services.AddCascadingAuthenticationState(); services.AddScoped(); services.AddAuthorization(); services.AddAuthentication(options => { options.DefaultScheme = Constants.AuthenticationScheme; }) .AddCookie(Constants.AuthenticationScheme) .AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { }) .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { }); services.ConfigureOqtaneCookieOptions(); services.ConfigureOqtaneAuthenticationOptions(configuration); services.AddOqtaneSiteOptions() .WithSiteIdentity() .WithSiteAuthentication(); services.AddCors(options => { options.AddPolicy(Constants.MauiCorsPolicy, policy => { // allow .NET MAUI client cross origin calls policy.WithOrigins("https://0.0.0.1", "http://0.0.0.1", "app://0.0.0.1") .AllowAnyHeader().AllowAnyMethod().AllowCredentials(); }); }); services.AddOutputCache(); services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); }) .AddOqtaneApplicationParts() // register any Controllers from custom modules .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes services.AddRazorPages(); services.AddRazorComponents() .AddInteractiveServerComponents(options => { if (environment.IsDevelopment()) { options.DetailedErrors = true; } }).AddHubOptions(options => { options.MaximumReceiveMessageSize = null; // no limit (for large amounts of data ie. textarea components) }) .AddInteractiveWebAssemblyComponents(); services.AddSwaggerGen(options => { options.CustomSchemaIds(type => type.ToString()); // Handle SchemaId already used for different type }); services.TryAddSwagger(configuration); return services; } public static IServiceCollection AddOqtaneAssemblies(this IServiceCollection services) { LoadAssemblies(); LoadSatelliteAssemblies(); services.AddOqtaneServices(); return services; } public static IServiceCollection AddOqtaneDbContext(this IServiceCollection services) { services.AddDbContext(options => { }, ServiceLifetime.Transient); services.AddDbContext(options => { }, ServiceLifetime.Transient); services.AddDbContextFactory(opt => { }, ServiceLifetime.Transient); return services; } public static OqtaneSiteOptionsBuilder AddOqtaneSiteOptions(this IServiceCollection services) { return new OqtaneSiteOptionsBuilder(services); } public static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return services; } public static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // providers services.AddScoped(); services.AddScoped(); return services; } public static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) { // services services.AddTransient(); services.AddTransient(); services.AddTransient(); // repositories services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // managers services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // obsolete services.AddTransient(); // replaced by ITenantManager return services; } public static IServiceCollection ConfigureOqtaneCookieOptions(this IServiceCollection services) { // note that ConfigureApplicationCookie internally uses an ApplicationScheme of "Identity.Application" services.ConfigureApplicationCookie(options => { options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.LoginPath = "/login"; // overrides .NET Identity default of /Account/Login options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; options.Events.OnRedirectToAccessDenied = context => { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; options.Events.OnRedirectToLogout = context => { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync; }); return services; } public static IServiceCollection ConfigureOqtaneAuthenticationOptions(this IServiceCollection services, IConfigurationRoot Configuration) { // prevent remapping of claims JwtSecurityTokenHandler.DefaultMapInboundClaims = false; // settings defined in appsettings services.Configure(Configuration); services.Configure(Configuration); return services; } public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services, IConfigurationRoot Configuration) { // default settings services.Configure(options => { // Password settings options.Password.RequireDigit = true; options.Password.RequiredLength = 6; options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; options.Password.RequiredUniqueChars = 1; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = false; // SignIn settings options.SignIn.RequireConfirmedEmail = false; options.SignIn.RequireConfirmedAccount = false; options.SignIn.RequireConfirmedPhoneNumber = false; // User settings options.User.RequireUniqueEmail = false; options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; }); // overrides defined in appsettings services.Configure(Configuration); return services; } public static IServiceCollection AddHttpClients(this IServiceCollection services) { if (!services.Any(x => x.ServiceType == typeof(HttpClient))) { services.AddScoped(provider => { var client = new HttpClient(new HttpClientHandler { UseCookies = false }); var httpContextAccessor = provider.GetRequiredService(); if (httpContextAccessor.HttpContext != null) { client.BaseAddress = new Uri(httpContextAccessor.HttpContext.Request.Scheme + "://" + httpContextAccessor.HttpContext.Request.Host); // set the cookies to allow HttpClient API calls to be authenticated foreach (var cookie in httpContextAccessor.HttpContext.Request.Cookies) { client.DefaultRequestHeaders.Add("Cookie", cookie.Key + "=" + WebUtility.UrlEncode(cookie.Value)); } } return client; }); } // register a named IHttpClientFactory services.AddHttpClient("oqtane", (provider, client) => { var httpContextAccessor = provider.GetRequiredService(); if (httpContextAccessor.HttpContext != null) { client.BaseAddress = new Uri(httpContextAccessor.HttpContext.Request.Scheme + "://" + httpContextAccessor.HttpContext.Request.Host); // set the cookies to allow HttpClient API calls to be authenticated foreach (var cookie in httpContextAccessor.HttpContext.Request.Cookies) { client.DefaultRequestHeaders.Add("Cookie", cookie.Key + "=" + WebUtility.UrlEncode(cookie.Value)); } } }); // IHttpClientFactory for calling remote services via RemoteServiceBase (not named = default) services.AddHttpClient(); return services; } public static IServiceCollection TryAddSwagger(this IServiceCollection services, IConfigurationRoot configuration) { if (configuration.GetSection("UseSwagger").Value != "false") { services.AddSwaggerGen(c => { c.SwaggerDoc(Constants.Version, new OpenApiInfo { Title = Constants.PackageId, Version = Constants.Version }); }); } return services; } private static IServiceCollection AddOqtaneServices(this IServiceCollection services) { if (services is null) { throw new ArgumentNullException(nameof(services)); } var hostedServiceType = typeof(IHostedService); var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); foreach (var assembly in assemblies) { // dynamically register module scoped services (ie. client service classes) var implementationTypes = assembly.GetInterfaces(); foreach (var implementationType in implementationTypes) { if (implementationType.AssemblyQualifiedName != null) { var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); services.AddScoped(serviceType ?? implementationType, implementationType); } } // dynamically register module transient services (ie. server DBContext, repository classes) implementationTypes = assembly.GetInterfaces(); foreach (var implementationType in implementationTypes) { if (implementationType.AssemblyQualifiedName != null) { var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); services.AddTransient(serviceType ?? implementationType, implementationType); } } // dynamically register hosted services var serviceTypes = assembly.GetTypes(hostedServiceType); foreach (var serviceType in serviceTypes) { if (!services.Any(item => item.ServiceType == serviceType)) { services.AddSingleton(hostedServiceType, serviceType); } } // dynamically register server startup services assembly.GetInstances() .ToList() .ForEach(x => x.ConfigureServices(services)); // dynamically register client startup services (these services will only be used when running on Blazor Server) assembly.GetInstances() .ToList() .ForEach(x => x.ConfigureServices(services)); } return services; } private static void LoadAssemblies() { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); if (assemblyPath == null) return; AssemblyLoadContext.Default.Resolving += ResolveDependencies; var assembliesFolder = new DirectoryInfo(assemblyPath); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); // iterate through Oqtane assemblies in /bin ( filter is narrow to optimize loading process ) foreach (var dll in assembliesFolder.EnumerateFiles($"*.dll", SearchOption.TopDirectoryOnly).Where(f => f.IsOqtaneAssembly())) { AssemblyName assemblyName; try { assemblyName = AssemblyName.GetAssemblyName(dll.FullName); } catch { Debug.WriteLine($"Oqtane Error: Cannot Get Assembly Name For {dll.Name}"); continue; } if (!assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.GetName()))) { AssemblyLoadContext.Default.LoadOqtaneAssembly(dll, assemblyName); } } } private static void LoadSatelliteAssemblies() { AssemblyLoadContext.Default.Resolving += ResolveDependencies; var installedCultures = LocalizationManager.GetSatelliteAssemblyCultures(); foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) { var code = Path.GetFileName(Path.GetDirectoryName(file)); if (installedCultures.Contains(code)) { try { Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(file))); Debug.WriteLine($"Oqtane Info: Loaded Satellite Assembly {file}"); } catch (Exception ex) { Debug.WriteLine($"Oqtane Error: Unable To Load Satellite Assembly {file} - {ex}"); } } else { Debug.WriteLine($"Oqtane Error: Culture Not Supported For Satellite Assembly {file}"); } } } private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name) { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + Path.DirectorySeparatorChar + name.Name + ".dll"; if (System.IO.File.Exists(assemblyPath)) { return context.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyPath))); } else { return null; } } } }