using FluentValidation; using FluentValidation.AspNetCore; using IdentityModel; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Localization; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.Reflection; using System.Security.Claims; using ZeroFramework.API.Infrastructure.Swagger; using ZeroFramework.IdentityServer.API.Constants; using ZeroFramework.IdentityServer.API.Extensions; using ZeroFramework.IdentityServer.API.IdentityStores; using ZeroFramework.IdentityServer.API.Infrastructure.Aliyun; using ZeroFramework.IdentityServer.API.Infrastructure.Authentication.Microsoft; using ZeroFramework.IdentityServer.API.Models.Generics; using ZeroFramework.IdentityServer.API.Services; using ZeroFramework.IdentityServer.API.Tenants; namespace ZeroFramework.IdentityServer.API { public class Startup(IConfiguration configuration) { public IConfiguration Configuration { get; } = configuration; // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddControllersWithViews(options => { options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; options.Filters.Add(); }).AddViewLocalization().AddDataAnnotationsLocalization(); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()).AddFluentValidationAutoValidation(); bool isDemoMode = Convert.ToBoolean(Configuration.GetRequiredSection("UseDemoLaunchMode").Value); services.AddDbContext((serviceProvider, optionsBuilder) => { optionsBuilder.UseSqlServer(Configuration.GetConnectionString("Default"), sqlOptions => { sqlOptions.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }); if (isDemoMode) { optionsBuilder.AddInterceptors(new DisalbeModifiedDeletedSaveChangesInterceptor()); } ICurrentTenant currentTenant = serviceProvider.GetRequiredService(); optionsBuilder.AddInterceptors(new CustomSaveChangesInterceptor(currentTenant), new CustomDbCommandInterceptor()); }); services.AddIdentity(options => { options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz@01234567890"; options.Password.RequireNonAlphanumeric = false; options.Password.RequireDigit = false; options.Password.RequiredLength = 3; options.Password.RequireUppercase = false; options.Password.RequireLowercase = false; }).AddEntityFrameworkStores() .AddDefaultTokenProviders().AddClaimsPrincipalFactory(); services.AddIdentityServer().AddAspNetIdentity() .AddSigningCredential(Certificates.Certificate.Get()) .AddConfigurationStore(options => { options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("Default"), options => { options.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); options.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }); }) .AddOperationalStore(options => { options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("Default"), options => { options.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); options.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }); }).AddProfileService(); services.AddTransient().AddTransient(); services.AddScoped().AddScoped(); services.Configure(options => { // This lambda determines whether user consent for non-essential // cookies is needed for a given request. options.CheckConsentNeeded = context => false; // requires using Microsoft.AspNetCore.Http; options.MinimumSameSitePolicy = SameSiteMode.None; }); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.TryAdd(JwtClaimTypes.Name, ClaimTypes.Name); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddJwtBearer(o => { o.Authority = Configuration["IdentityServer:AuthorizationUrl"]; o.TokenValidationParameters.ValidateAudience = false; o.TokenValidationParameters.NameClaimType = ClaimTypes.Name; }) .AddMicrosoftAccount(microsoftOptions => { microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ClientId"]!; microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"]!; microsoftOptions.EventsType = typeof(CustomOAuthAuthenticationEvents); //.AspNetCore.Correlation. state property not found microsoftOptions.RemoteAuthenticationTimeout = TimeSpan.FromDays(15); microsoftOptions.CorrelationCookie.SameSite = SameSiteMode.Lax; }).AddQQ(qqOptions => { qqOptions.ClientId = Configuration["Authentication:TencentQQ:AppID"]!; qqOptions.ClientSecret = Configuration["Authentication:TencentQQ:AppKey"]!; qqOptions.EventsType = typeof(CustomOAuthAuthenticationEvents); qqOptions.RemoteAuthenticationTimeout = TimeSpan.FromDays(15); qqOptions.CorrelationCookie.SameSite = SameSiteMode.Lax; }).AddGitHub(gitHubOptions => { gitHubOptions.ClientId = Configuration["Authentication:GitHub:ClientID"]!; gitHubOptions.ClientSecret = Configuration["Authentication:GitHub:ClientSecret"]!; gitHubOptions.EventsType = typeof(CustomOAuthAuthenticationEvents); gitHubOptions.RemoteAuthenticationTimeout = TimeSpan.FromDays(15); gitHubOptions.CorrelationCookie.SameSite = SameSiteMode.Lax; }).AddWeibo("Weibo", "ΞΆ²©", weiboOptions => { weiboOptions.ClientId = Configuration["Authentication:Weibo:AppKey"]!; weiboOptions.ClientSecret = Configuration["Authentication:Weibo:AppSecret"]!; weiboOptions.EventsType = typeof(CustomOAuthAuthenticationEvents); weiboOptions.UserEmailsEndpoint = string.Empty; weiboOptions.RemoteAuthenticationTimeout = TimeSpan.FromDays(15); weiboOptions.CorrelationCookie.SameSite = SameSiteMode.Lax; }).AddWeixin("WeChat", "ΞΆΠΕ", weChatOptions => { weChatOptions.ClientId = Configuration["Authentication:WeChat:AppID"]!; weChatOptions.ClientSecret = Configuration["Authentication:WeChat:AppSecret"]!; weChatOptions.EventsType = typeof(CustomOAuthAuthenticationEvents); weChatOptions.RemoteAuthenticationTimeout = TimeSpan.FromDays(15); weChatOptions.CorrelationCookie.SameSite = SameSiteMode.Lax; }); services.Configure(Configuration.GetSection("AlibabaCloud")); services.AddTransient(); services.AddHttpClient("aliyun").AddHttpMessageHandler(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle services.AddEndpointsApiExplorer(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "IdentityServer API", Version = "v1" }); c.SupportNonNullableReferenceTypes(); c.CustomOperationIds(api => { string? actionName = api.ActionDescriptor.RouteValues["action"]; if (actionName is not null) { return $"{System.Text.Json.JsonNamingPolicy.CamelCase.ConvertName(actionName)}"; } return api.ActionDescriptor.Id; }); string? identityServer = Configuration.GetValue("IdentityServer:AuthorizationUrl"); c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, Flows = new OpenApiOAuthFlows { AuthorizationCode = new OpenApiOAuthFlow { AuthorizationUrl = new Uri($"{identityServer}/connect/authorize"), TokenUrl = new Uri($"{identityServer}/connect/token"), Scopes = new Dictionary { { "openid", "Your user identifier" }, { "identityserver", "Identity server api" } } } } }); c.OperationFilter(); c.OperationFilter(); c.MapType>(() => new OpenApiSchema { Type = "string", Format = "json" }); }); services.AddTransient(); services.AddAuthorization(options => { options.AddPolicy(AuthorizeConstants.TenantOwnerPolicyName, policy => policy.AddRequirements(new OperationAuthorizationRequirement())); }); services.AddCors(options => { string[]? allowedOrigins = Configuration.GetSection("AllowedOrigins").Get(); if (allowedOrigins is not null) { options.AddDefaultPolicy(builder => builder.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader().AllowCredentials()); } }); string[] supportedCultures = new[] { "zh-CN", "en-US" }; services.AddRequestLocalization(options => { options.ApplyCurrentCultureToResponseHeaders = false; options.SetDefaultCulture(supportedCultures.First()); options.AddSupportedCultures(supportedCultures); options.AddSupportedUICultures(supportedCultures); options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context => { if (context.Request.Headers.TryGetValue("Culture", out var values)) { return await Task.FromResult(new ProviderCultureResult(values.First())); } return null; })); }); services.AddAutoMapper(Assembly.GetExecutingAssembly()); ValidatorOptions.Global.DisplayNameResolver = (t, m, l) => m.GetCustomAttribute()?.Name ?? m.GetCustomAttribute()?.DisplayName ?? m.Name; services.AddTenantMiddleware(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(WebApplication app, IWebHostEnvironment env) { //Configure ASP.NET Core to work with proxy servers and load balancers app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All }); app.UseAuthentication(); app.UseTenantMiddleware(); app.UseRequestLocalization(); SampleDataSeed.SeedAsync(app).Wait(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; } else { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "IdentityServer API v1"); c.DocumentTitle = "IdentityServer API Document"; c.IndexStream = () => GetType().Assembly.GetManifestResourceStream($"{GetType().Assembly.GetName().Name}.Infrastructure.Swagger.Index.html"); c.OAuthClientId("identityserverswagger"); c.OAuthClientSecret("secret"); c.OAuthAppName("Identity Server Swagger"); c.OAuthUsePkce(); }); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCors(); app.UseIdentityServer(); // Fix a problem with chrome. Chrome enabled a new feature "Cookies without SameSite must be secure", // the coockies shold be expided from https, but in eShop, the internal comunicacion in aks and docker compose is http. // To avoid this problem, the policy of cookies shold be in Lax mode. app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); app.UseRouting(); app.UseAuthorization(); app.MapDefaultControllerRoute(); } } }