/* This file is part of the Notesnook Sync Server project (https://notesnook.com/) Copyright (C) 2023 Streetwriters (Private) Limited This program is free software: you can redistribute it and/or modify it under the terms of the Affero GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Affero GNU General Public License for more details. You should have received a copy of the Affero GNU General Public License along with this program. If not, see . */ using System; using System.IO; using System.Security.Claims; using System.Threading.RateLimiting; using AspNetCore.Identity.Mongo; using IdentityServer4.MongoDB.Entities; using IdentityServer4.MongoDB.Interfaces; using IdentityServer4.MongoDB.Options; using IdentityServer4.MongoDB.Stores; using IdentityServer4.ResponseHandling; using IdentityServer4.Services; using IdentityServer4.Stores; using IdentityServer4.Validation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using MongoDB.Bson.Serialization; using Quartz; using Streetwriters.Common; using Streetwriters.Common.Extensions; using Streetwriters.Common.Interfaces; using Streetwriters.Common.Messages; using Streetwriters.Common.Models; using Streetwriters.Common.Services; using Streetwriters.Identity.Helpers; using Streetwriters.Identity.Interfaces; using Streetwriters.Identity.Jobs; using Streetwriters.Identity.Services; using Streetwriters.Identity.Validation; using IdentityServer4.MongoDB.Configuration; namespace Streetwriters.Identity { public class Startup { public Startup(IConfiguration configuration, IWebHostEnvironment environment) { Configuration = configuration; WebHostEnvironment = environment; } private IConfiguration Configuration { get; } private IWebHostEnvironment WebHostEnvironment { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { var connectionString = Constants.MONGODB_CONNECTION_STRING; services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient, Argon2PasswordHasher>(); services.AddDefaultCors(); //services.AddSingleton(); services.AddIdentityMongoDbProvider(options => { // Password settings. options.Password.RequireDigit = false; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; options.Password.RequiredLength = 8; options.Password.RequiredUniqueChars = 0; // Lockout settings. options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; // User settings. //options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._"; options.User.RequireUniqueEmail = true; options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultPhoneProvider; }, (options) => { options.RolesCollection = "roles"; options.UsersCollection = "users"; // options.MigrationCollection = "migration"; options.ConnectionString = connectionString; }).AddDefaultTokenProviders(); services.AddIdentityServer( options => { options.Events.RaiseSuccessEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseErrorEvents = true; options.IssuerUri = Servers.IdentityServer.ToString(); }) .AddExtensionGrantValidator() .AddExtensionGrantValidator() .AddExtensionGrantValidator() .AddConfigurationStore(options => { options.ConnectionString = connectionString; }) .AddAspNetIdentity() .AddInMemoryClients(Config.Clients) .AddInMemoryApiResources(Config.ApiResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryIdentityResources(Config.IdentityResources) .AddKeyManagement() .AddFileSystemPersistence(Path.Combine(WebHostEnvironment.ContentRootPath, @"keystore")); services.Configure(options => { options.ConnectionString = connectionString; }); services.Configure(options => { options.TokenLifespan = TimeSpan.FromHours(2); }); services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddSlidingWindowLimiter("strict", options => { options.PermitLimit = 30; options.Window = TimeSpan.FromSeconds(60); options.SegmentsPerWindow = 10; options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; options.QueueLimit = 0; }); options.AddPolicy("super_strict", (context) => { var key = context.User?.FindFirstValue("sub") ?? "default"; return RateLimitPartition.GetSlidingWindowLimiter(key, (key) => new SlidingWindowRateLimiterOptions { PermitLimit = 6, Window = TimeSpan.FromMinutes(1), SegmentsPerWindow = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = int.MaxValue, AutoReplenishment = true }); }); }); services.AddAuthorizationBuilder().AddPolicy("mfa", policy => { policy.AddAuthenticationSchemes("Bearer+jwt"); policy.RequireClaim("scope", Config.MFA_GRANT_TYPE_SCOPE); }); services.AddLocalApiAuthentication(); services.AddAuthentication() .AddJwtBearer("Bearer+jwt", options => { options.MapInboundClaims = false; options.Authority = Servers.IdentityServer.ToString(); options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidTypes = ["at+jwt"], ValidateAudience = false, ValidateIssuerSigningKey = true, ValidateIssuer = true, }; }); services.AddQuartzHostedService(q => { q.WaitForJobsToComplete = true; q.AwaitApplicationStarted = true; q.StartDelay = TimeSpan.FromMinutes(1); }); AddOperationalStore(services, new TokenCleanupOptions { Enable = true, Interval = 3600 * 12 }); services.AddScoped(); services.AddScoped(); services.AddTransient(); services.AddControllers(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddHealthChecks(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseForwardedHeadersWithKnownProxies(env, "CF-Connecting-IP"); app.UseCors("notesnook"); app.UseVersion(Servers.IdentityServer); app.UseRouting(); app.UseIdentityServer(); app.UseRateLimiter(); app.UseAuthorization(); app.UseAuthentication(); app.UseWamp(WampServers.IdentityServer, (realm, server) => { realm.Services.RegisterCallee(() => app.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService()); realm.Subscribe(SubscriptionServerTopics.CreateSubscriptionTopic, async (Subscription subscription) => { using (var serviceScope = app.ApplicationServices.CreateScope()) { var services = serviceScope.ServiceProvider; var userManager = services.GetRequiredService>(); await MessageHandlers.CreateSubscription.Process(subscription, userManager); } }); realm.Subscribe(SubscriptionServerTopics.DeleteSubscriptionTopic, async (DeleteSubscriptionMessage message) => { using (var serviceScope = app.ApplicationServices.CreateScope()) { var services = serviceScope.ServiceProvider; var userManager = services.GetRequiredService>(); await MessageHandlers.DeleteSubscription.Process(message, userManager); } }); }); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapHealthChecks("/health"); }); } private static void AddOperationalStore(IServiceCollection services, TokenCleanupOptions? tokenCleanUpOptions = null) { BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); cm.SetIgnoreExtraElements(true); }); services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionJobFactory(); if (tokenCleanUpOptions?.Enable == true) { var jobKey = new JobKey("TokenCleanupJob"); q.AddJob(opts => opts.WithIdentity(jobKey)); q.AddTrigger(opts => opts .ForJob(jobKey) .WithIdentity("TokenCleanup-trigger") .WithSimpleSchedule((s) => s.RepeatForever().WithIntervalInSeconds(tokenCleanUpOptions.Interval))); } }); } } }