azure - facebook 或 google 登录时 Aspnetcore 关联失败,确认电子邮件时 token 无效

标签 azure asp.net-core asp.net-identity

我已经将我的应用程序迁移到了 aspnetcore,现在我确实遇到了验证 token 的随机问题。 1. 问题是用户随机收到

An error was encountered while handling the remote login. Correlation failed.

问题是,如果我自己去测试它,它就会起作用。

第二个问题是,当用户收到电子邮件确认 token 并单击电子邮件中的链接时,他们将得到

invalid token

因此他们无法确认电子邮件。

首先,我认为问题出在 UseCookiePolicy 上,但我已禁用它。

启动.cs

namespace Flymark.Online.Web
{
    public class Startup
    {
        private readonly IHostingEnvironment _env;

        public Startup(IHostingEnvironment env)
        {
            _env = env;
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", true, true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }


        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Configure SnapshotCollector from application settings
            services.Configure<SnapshotCollectorConfiguration>(
                Configuration.GetSection(nameof(SnapshotCollectorConfiguration)));
            // Add SnapshotCollector telemetry processor.
            services.AddSingleton<ITelemetryProcessorFactory>(sp => new SnapshotCollectorTelemetryProcessorFactory(sp));
            services.AddApplicationInsightsTelemetryProcessor<TelemetryFilter>();
            services.AddSingleton<ITelemetryInitializer, AppInsightsInitializer>();
            services.AddCors();
            var decompressionOptions = new RequestDecompressionOptions();

            decompressionOptions.UseDefaults();
            services.AddRequestDecompression(decompressionOptions);

            FlymarkAppSettings.Init(Configuration, _env.EnvironmentName);

            var storageUri = new Uri(Configuration.GetValue<string>("Flymark:DataProtectionStorageUrl"));
            //Get a reference to a container to use for the sample code, and create it if it does not exist.
            var container = new CloudBlobClient(storageUri).GetContainerReference("data-protection");
            services.AddDataProtection()
                .SetApplicationName("Flymark.Online")
                .PersistKeysToAzureBlobStorage(container, "data-protection.xml");

            services.AddDetection();
            services.AddAutoMapper();

            services.AddWebMarkupMin(
                    options =>
                    {
                        options.AllowMinificationInDevelopmentEnvironment = true;
                        options.AllowCompressionInDevelopmentEnvironment = true;
                    })
                .AddHtmlMinification(o =>
                {
                    o.ExcludedPages = new List<IUrlMatcher>
                    {
                        new WildcardUrlMatcher("/scripts/*")
                    };
                    o.MinificationSettings.AttributeQuotesRemovalMode = HtmlAttributeQuotesRemovalMode.KeepQuotes;
                    o.MinificationSettings.EmptyTagRenderMode = HtmlEmptyTagRenderMode.NoSlash;
                    o.MinificationSettings.RemoveOptionalEndTags = false;
                })
                .AddXmlMinification()
                .AddHttpCompression();
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.Lax;
            });

            services
                .AddScoped<UserStore<ApplicationUser, IdentityRole<int>, FlymarkContext, int, IdentityUserClaim<int>,
                        IdentityUserRole<int>, IdentityUserLogin<int>, IdentityUserToken<int>, IdentityRoleClaim<int>>,
                    ApplicationUserStore>();
            services.AddScoped<UserManager<ApplicationUser>, FlymarkUserManager>();
            services.AddScoped<RoleManager<IdentityRole<int>>, ApplicationRoleManager>();
            services.AddScoped<SignInManager<ApplicationUser>, ApplicationSignInManager>();
            services
                .AddScoped<RoleStore<IdentityRole<int>, FlymarkContext, int, IdentityUserRole<int>,
                    IdentityRoleClaim<int>>, ApplicationRoleStore>();
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            services.AddIdentity<ApplicationUser, IdentityRole<int>>(
                    o =>
                    {
                        o.User.RequireUniqueEmail = true; 
                    })
                .AddUserStore<ApplicationUserStore>()
                .AddUserManager<FlymarkUserManager>()
                .AddRoleStore<ApplicationRoleStore>()
                .AddRoleManager<ApplicationRoleManager>()
                .AddSignInManager<ApplicationSignInManager>()
                .AddClaimsPrincipalFactory<FlymarkClaimsPrincipalFactory>()
                .AddDefaultTokenProviders();
            services.AddSingleton<ILoggerFactory, LoggerFactory>(sp =>
                new LoggerFactory(
                    sp.GetRequiredService<IEnumerable<ILoggerProvider>>(),
                    sp.GetRequiredService<IOptionsMonitor<LoggerFilterOptions>>()
                )
            );
            services.Configure<ApiBehaviorOptions>(options => { options.SuppressModelStateInvalidFilter = true; });
            services.AddMemoryCache();
            services.AddSingleton<IEmailSender, FlymarkEmailSender>();

            services.AddMvc(o =>
                {
                    o.Conventions.Add(new FlymarkAsyncConvention());
                    o.AllowValidatingTopLevelNodes = false;
                    o.AllowEmptyInputInBodyModelBinding = true;
                })
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
                .AddJsonOptions(opt =>
                {
                    opt.SerializerSettings.DateFormatString = "dd/MM/yyyy";
                    opt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
                    var resolver = opt.SerializerSettings.ContractResolver;
                    if (resolver == null) return;
                    if (resolver is DefaultContractResolver res) res.NamingStrategy = null;
                });
            services.Configure<IdentityOptions>(options =>
            {
                // Default Password settings.
                options.Password.RequireDigit = false;
                options.Password.RequireLowercase = false;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequiredLength = 6;
                options.Password.RequiredUniqueChars = 1;
                options.Lockout.MaxFailedAccessAttempts = 20;
            });
            services
                .AddAuthorization(options =>
                {
                    options.DefaultPolicy = new AuthorizationPolicyBuilder()
                        .AddAuthenticationSchemes(OAuthValidationDefaults.AuthenticationScheme,
                            IdentityConstants.ApplicationScheme)
                        .RequireAuthenticatedUser()
                        .Build();
                });
            services.AddAuthentication()
                .AddExternalAuthProviders(Configuration)
                .AddFlymarkOpenIdConnectServer()
                .AddOAuthValidation(OAuthValidationDefaults.AuthenticationScheme);

            services.Configure<SecurityStampValidatorOptions>(options =>
            {
                // This is the key to control how often validation takes place
                options.ValidationInterval = TimeSpan.FromMinutes(15);
            });
            services.ConfigureApplicationCookie(config =>
            {
                config.LoginPath = "/Identity/Account/LogIn";
                config.AccessDeniedPath = "/Identity/Account/LogIn";
                config.SlidingExpiration = true;
                config.Events.OnRedirectToLogin = OnRedirectToLoginAsync;
            });
        }
        private Task OnRedirectToLoginAsync(RedirectContext<CookieAuthenticationOptions> context)
        {
            if (context.HttpContext.Request.Path.Value.Contains("/api"))
                context.Response.StatusCode = 401;
            else
                context.Response.Redirect(context.RedirectUri);

            return Task.CompletedTask;
        }

        public void ConfigureContainer(ContainerBuilder builder)
        {
            builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());

            //builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
            builder.RegisterModule(new FlymarkDalDiModule
            {
                Configuration = Configuration
            });
            builder.RegisterModule(new DbDiModule(FlymarkAppSettings.Instance.DbContextConnection,
                FlymarkAppSettings.Instance.StorageConnectionString));
            builder.RegisterModule<FlymarkWebDiModule>();
        }

        private CultureInfo CreateCulture(string key)
        {
            return new CultureInfo(key)
            {
                NumberFormat = {NumberDecimalSeparator = "."},
                DateTimeFormat = {ShortDatePattern = "dd/MM/yyyy"}
            };
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env,
            ILoggerFactory loggerFactory, IMapper mapper)
        {
#if DEBUG
            mapper.ConfigurationProvider.AssertConfigurationIsValid();
#endif

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseStaticFiles(new StaticFileOptions
                {
                    OnPrepareResponse = context =>
                    {
                        context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
                        context.Context.Response.Headers.Add("Expires", "-1");
                    }
                });
            }
            else
            {
                app.UseExceptionHandler("/Error/Error500");
                app.UseStaticFiles();
            }

            app.UseCors(builder =>
            {
                builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowCredentials()
                    .SetPreflightMaxAge(TimeSpan.FromMinutes(5))
                    .AllowAnyHeader();
            });
            app.UseRequestDecompression();
            app.UseLegacyTokenContentTypeFixMiddleware();
            var supportedCultures = new[]
            {
                CreateCulture("en"),
                CreateCulture("ru"),
                CreateCulture("uk")
            };
            app.UseFlymarkExceptionMiddleware();
            app.UseCookiePolicy();
            app
                .UseAuthentication()
                .UseDomainMiddleware()
                .UseRequestLocalization(new RequestLocalizationOptions
                {
                    DefaultRequestCulture = new RequestCulture("en"),
                    SupportedCultures = supportedCultures,
                    SupportedUICultures = supportedCultures
                })
                .UseWebMarkupMin();

            app.Use(async (ctx, next) =>
            {
                await next();

                if (ctx.Response.StatusCode == 404 && !ctx.Response.HasStarted)
                {
                    //Re-execute the request so the user gets the error page
                    var originalPath = ctx.Request.Path.Value;
                    ctx.Items["originalPath"] = originalPath;
                    ctx.Request.Path = "/error/error404";
                    await next();
                }
            });
            app
                .UseMvc(routes =>
                {
                    routes.MapRoute(
                        "areaRoute",
                        "{area:exists}/{controller=Dashboard}/{action=Index}/{id?}");
                    routes.MapRoute(
                        "default",
                        "{controller=Home}/{action=Index}/{id?}");
                });
        }
    }
}

我正在生成电子邮件确认的网址,如下所示:

var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);

var callbackUrl = Url.Page("/Account/ConfirmEmail",
                        null,
                        new {userId = user.Id, code = code.ToBase64String()},
                        returnDomainUrl.Scheme,
                        returnDomainUrl.Host);

我还认为它可能是一个 angularjs(我的页面上仍然有它),但它没有在/signin-facebook 加载,因为它是由中间件处理的。

我认为问题出在数据保护方面,因为我在登录和确认电子邮件中收到了它们

我也尝试过基于 64 电子邮件 token ,但它不会有帮助,此外我认为 url 是由 Page.Url 自动编码的

最佳答案

token 验证很可能失败,因为 token 是在一个域中生成并在另一个域中验证的。

在 ASP.Net 中,可以通过在两个域的 web.config 文件中使用相同的 machineKey 来解决此问题。

对于 ASP.Net Core,您可以按照描述替换 machineKey here因此您在两个域中具有相同的加密设置。

参见:Replace the ASP.NET machineKey in ASP.NET Core

关于azure - facebook 或 google 登录时 Aspnetcore 关联失败,确认电子邮件时 token 无效,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54411260/

相关文章:

c# - ASP.Net Core MVC Identity - 添加临时( session )声明

azure - 写入表存储,不断将实体从一个表替换到另一个表

linux - 获取 Visual Studio 用于构建容器的命令

c# - 相关 ICollection 实体不能为空

c# - 在 Controller 级别设置 NullValueHandling

c# - 如何模拟自定义 UserStore 和 RoleStore

asp.net-identity - 使用 Identity 配置 IdentityServer 4,结合 services.AddAuthentication() 和 services.AddIdentity()

azure - 将azure Web应用程序添加到应用程序服务环境中

Azure DevOps 构造命名约定

Python SDK Azure 计算机视觉 : 'bytes' object has no attribute 'read'