zeroframework/Services/Identity/ZeroFramework.IdentityServer.API/Startup.cs
2023-12-05 17:22:48 +08:00

297 lines
15 KiB
C#
Raw Permalink Blame History

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<HttpResponseExceptionFilter>();
}).AddViewLocalization().AddDataAnnotationsLocalization();
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()).AddFluentValidationAutoValidation();
bool isDemoMode = Convert.ToBoolean(Configuration.GetRequiredSection("UseDemoLaunchMode").Value);
services.AddDbContext<ApplicationDbContext>((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<ICurrentTenant>();
optionsBuilder.AddInterceptors(new CustomSaveChangesInterceptor(currentTenant), new CustomDbCommandInterceptor());
});
services.AddIdentity<ApplicationUser, ApplicationRole>(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<ApplicationDbContext>()
.AddDefaultTokenProviders().AddClaimsPrincipalFactory<CustomUserClaimsPrincipalFactory>();
services.AddIdentityServer().AddAspNetIdentity<ApplicationUser>()
.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<IdentityProfileService>();
services.AddTransient<IEmailSender, AuthMessageSender>().AddTransient<ISmsSender, AuthMessageSender>();
services.AddScoped<CustomCookieAuthenticationEvents>().AddScoped<CustomOAuthAuthenticationEvents>();
services.Configure<CookiePolicyOptions>(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", <><CEA2>", 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", <><CEA2>", 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<AlibabaCloudOptions>(Configuration.GetSection("AlibabaCloud"));
services.AddTransient<AliyunAuthHandler>();
services.AddHttpClient("aliyun").AddHttpMessageHandler<AliyunAuthHandler>();
// 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<string>("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<string, string>
{
{ "openid", "Your user identifier" },
{ "identityserver", "Identity server api" }
}
}
}
});
c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<CamelCaseNamingOperationFilter>();
c.MapType<IEnumerable<SortingDescriptor>>(() => new OpenApiSchema { Type = "string", Format = "json" });
});
services.AddTransient<IAuthorizationHandler, PermissionRequirementHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizeConstants.TenantOwnerPolicyName, policy => policy.AddRequirements(new OperationAuthorizationRequirement()));
});
services.AddCors(options =>
{
string[]? allowedOrigins = Configuration.GetSection("AllowedOrigins").Get<string[]>();
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<DisplayAttribute>()?.Name ?? m.GetCustomAttribute<DisplayNameAttribute>()?.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();
}
}
}