dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

GeneratePasswordResetTokenAsync generated token does not work from other domain #9887

Closed vankampenp closed 4 years ago

vankampenp commented 5 years ago

Describe the bug

Not sure it is a bug or a missing feature. We have two identical websites using the same underlying database. The only difference is the language. For example www.example.fr and www.example.nl

I would like to have one admin location, generating users, or resetting passwords. However, when the token is created on one web site, it does not work on the other.

To Reproduce

Steps to reproduce the behavior:

  1. Using ASP.NET Core 2.2

  2. Run this code on domain: www.example.fr var token = await _userManager.GeneratePasswordResetTokenAsync(user);

  3. Construct a link for the token including the second domain: var link = "https://www.example.nl/account/resettoken?token=token&email=email Email this link to the user

  4. On the other domain (/www.example.nl), when the link is clicked, get the user: var user = await _userManager.FindByEmailAsync(email); (Note: the database is the same, so the user is known here too)

and verify the token:

bool success = await _userManager.VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, "ResetPassword", token);

The result is an invalid token. If I take the link form the email and change the domain www.example.nl to www.example.fr, the token is validated successfully

Expected behavior

I did not expect the current domain on which the token is generated to be part of the check in the token.

Is this intended behavior? If so would it be possible to generate a verification token for another domain?

Thanks, Pieter

blowdart commented 5 years ago

This is not the expected behaviour at all, which is very weird. Are you setting a static data protection app key here?

blowdart commented 5 years ago

We're closing this issue as no response or updates have been provided in a timely manner. If you have more details and are encountering this issue please add a new reply and re-open the issue.

vankampenp commented 5 years ago

Sorry for the delay. Yes, I am using a state data protection app key, which is generated if it is not there. Will it work if I copy the key from one to the other?

jbartolome commented 5 years ago

@blowdart I'm running into the exact same problem. Should I open a new issue for this since this one is closed?

Thanks, Jennifer

blowdart commented 5 years ago

@jbartolome Let's reopen this one.

Can I get the startup.cs, with any secrets removed as a starting point?

vankampenp commented 5 years ago

Here is mine:

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Formatters;

using Microsoft.EntityFrameworkCore;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using DTOWEB.Areas.api.Models;
using DTOWEB.Infrastructure;
using DTOWEBInfra;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using NLog;
using WebMarkupMin.AspNetCore2;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.SignalR;
using Microsoft.IdentityModel.Tokens;
using Joonasw.AspNetCore.SecurityHeaders;
using Microsoft.OpenApi.Models;
using Utility = DTOWEBInfra.Utility;

namespace DTOWEB
{
    //confirm e-mail should have a longer duration
    //from https://github.com/aspnet/Identity/issues/859
    public class ConfirmEmailDataProtectorTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
    {
        public ConfirmEmailDataProtectorTokenProvider(IDataProtectionProvider dataProtectionProvider, IOptions<ConfirmEmailDataProtectionTokenProviderOptions> options) : base(dataProtectionProvider, options)
        {
        }
    }
    public class ConfirmEmailDataProtectionTokenProviderOptions : DataProtectionTokenProviderOptions { }

    public class Startup
    {
        private const string EmailConfirmationTokenProviderName = "ConfirmEmail";

        public Startup(IConfiguration configuration)
        {

            Configuration = configuration;
            var ver = Assembly.GetEntryAssembly().GetName().Version.ToString();
            var dir = Environment.CurrentDirectory;
            var sql = Configuration["AppSettings:sql"];

            //create the key, zet het in {production/development}-configurationstring

            var dbConnectionString = Configuration[$"settings:{sql}"];

            string[] hosts = Directory.GetCurrentDirectory().Split('\\');
            var host = hosts[hosts.Length - 1] == "httpdocs" ? hosts[hosts.Length - 2] : hosts[hosts.Length - 1];
            RootLanguage rootLanguage = new RootLanguage();
            Configuration.GetSection("RootLanguage").Bind(rootLanguage);
            RootLangBase rootLangBase = new RootLangBase();
            Configuration.GetSection("RootLangBase").Bind(rootLangBase);

            var appSettings = new AppSettings();
            Configuration.GetSection("AppSettings").Bind(appSettings);
            ;
            Config.Settings = new Config
            {
                Environment = Configuration["AppSettings:environment"].ToLower(),
                DropDirectory = Path.Combine(dir, "logs"),
                DebugLog = Utility.ToInt(Configuration["AppSettings:debugLog"]),
                MachineName = host,
                DtoVersion = ver,
                ConfigurationString = dbConnectionString,
                MaxFailedAccessAttempts = Utility.ToInt(Configuration["AppSettings:MaxFailedAccessAttempts"]),
                AppSettings = appSettings,
                LanguageNl = rootLanguage.LanguageNl,
                LanguageEn = rootLanguage.LanguageEn,
                LangBaseNl = rootLangBase.LanguageNl,
                LangBaseEn = rootLangBase.LanguageEn,
                SiteLanguage = Configuration["AppSettings:siteLanguageId"] == "en" ? rootLanguage.LanguageEn : rootLanguage.LanguageNl,
                SiteDto = Configuration["AppSettings:siteLanguageId"] == "nl" ? Configuration["AppSettings:siteDto"] : Configuration["AppSettings:siteDtoEn"]
            };

        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            //antiforgery working:
            services.AddDataProtection()
                .PersistKeysToFileSystem(new DirectoryInfo(Config.Settings.DropDirectory));

            //// Adds services required for using options.
            services.AddOptions();

            services.AddDistributedMemoryCache(); // Adds a default in-memory implementation of IDistributedCache
            services.AddSingleton(Configuration);
            // Register the IConfiguration instance which AppSettings binds against.
            services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
            services.Configure<RootLanguage>(Configuration.GetSection("RootLanguage"));

            services.AddWebMarkupMin(options =>
                {
                    options.AllowMinificationInDevelopmentEnvironment = true;
                    options.AllowCompressionInDevelopmentEnvironment = true;
                })
                .AddHtmlMinification()
                .AddXmlMinification();

            services.AddDbContext<DtoIdentityDbContext>(options =>
                options.UseSqlServer(Config.EntityConnectionString));

            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => false;
                options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;
            });

            //add class to inject the cookies in to the logged in user
            services.AddScoped<IUserClaimsPrincipalFactory<DtoUser>, DtoClaimsPrincipalFactory>();

            services.AddAuthorization(options =>
            {
                //only managers allowed
                options.AddPolicy("ManagersOnly", policy =>
                {
                    policy.RequireClaim(DtoClaimTypes.IsManager, "true");
                    policy.RequireRole(DtoRoles.Examiner);
                    policy.RequireAuthenticatedUser();
                });
                options.AddPolicy("ReadWriteOnly", policy =>
                {
                    policy.RequireClaim(DtoClaimTypes.IsReadWrite, "true");
                    policy.RequireRole(DtoRoles.Examiner);
                    policy.RequireAuthenticatedUser();
                });
                options.AddPolicy("EpdUser", policy => policy.RequireClaim(DtoClaimTypes.EpdUser, "true"));

            });

            services.AddIdentity<DtoUser, IdentityRole>(options =>
            // Password settings
            {
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 8;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = true;
                // Lockout settings

                options.Lockout.DefaultLockoutTimeSpan =
                    TimeSpan.FromMinutes(Utility.ToInt(Configuration["AppSettings:DefaultLockoutTimeSpan"]));
                options.Lockout.MaxFailedAccessAttempts =
                    Utility.ToInt(Configuration["AppSettings:MaxFailedAccessAttempts"]);

                // User settings
                options.User.RequireUniqueEmail = true;

                options.Tokens.EmailConfirmationTokenProvider = EmailConfirmationTokenProviderName;
            })
                .AddEntityFrameworkStores<DtoIdentityDbContext>()
                .AddDefaultTokenProviders();

            services.ConfigureApplicationCookie(options =>
            {
                options.AccessDeniedPath = "/index?relogin=1";
                options.ExpireTimeSpan =
                    TimeSpan.FromMinutes(Utility.ToInt(Configuration["AppSettings:ExpireTimeSpan"]));
                options.LoginPath = "/";

                options.SlidingExpiration = true;
                options.LoginPath = "/globalerr";
                options.LogoutPath = "/";
                options.AccessDeniedPath = "/globalerr";
                options.Cookie = new CookieBuilder
                {
                    Name = "Testmij",
                    HttpOnly = true,
                    SecurePolicy = CookieSecurePolicy.SameAsRequest,

                };

            });
            if (Configuration.GetChildren().Any(x => x.Key == "Authentication"))
            {
                services.AddAuthentication()
                    .AddFacebook(options =>
                    {
                        options.AppId = Configuration["Authentication:Facebook:AppId"];
                        options.AppSecret =Configuration["Authentication:Facebook:AppSecret"];
                    })
                    .AddTwitter(options =>
                    {
                        options.ConsumerKey =Configuration["Authentication:Twitter:ConsumerKey"];
                        options.ConsumerSecret =
                           Configuration["Authentication:Twitter:ConsumerSecret"];
                    })
                    .AddGoogle(options =>
                    {
                        options.ClientId = Configuration["Authentication:Google:ClientId"];
                        options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
                    });
            }

            services.AddAuthentication().AddJwtBearer(options =>
            {
                options.RequireHttpsMetadata = true;
                options.SaveToken = true;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidIssuer = Configuration["JwtAuthentication:ValidAudience"],
                    ValidAudience = Configuration["JwtAuthentication:ValidIssuer"],
                    ValidateIssuerSigningKey = true,
                    ValidateLifetime = true,
                    IssuerSigningKey =
                        new SymmetricSecurityKey(
                            Encoding.UTF8.GetBytes(Configuration["JwtAuthentication:SecurityKey"])),
                };

            });
            services.AddHttpClient();
            services.AddHttpClient("zorgmail", c =>
            {
                c.BaseAddress = new Uri(Configuration["zorgmail:url"]);
                c.DefaultRequestHeaders.Accept.Add(
                    new MediaTypeWithQualityHeaderValue("text/xml"));

            });
            services.AddSignalR(); //Communication with the client

            //Get who the owner is when creating the connection, with a custom provider (OwnerIdProvider)
            services.AddSingleton<IUserIdProvider, OwnerIdProvider>(); //register the custom userId provider for owners

            services.Configure<ConfirmEmailDataProtectionTokenProviderOptions>(options =>
                {
                    options.TokenLifespan =
                        TimeSpan.FromDays(Utility.ToInt(Configuration["AppSettings:EmailTokenLifespanDays"]));
                });
            services.Configure<JwtAuthentication>(Configuration.GetSection("JwtAuthentication"));

            //add the hosted background service and queue
            services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
            services.AddHostedService<QueuedHostedService>();

            // Add MVC services to the services container.
            services.AddMvc(options =>
                {
                    options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
                    options.EnableEndpointRouting = false;  //DTO-255 https://github.com/aspnet/AspNetCore/issues/6415
                    options.Filters.Add<OperationCancelledExceptionFilterAttribute>(); //globally handle cancellation
                     options.Conventions.Add(new ApiExplorerGroupPerVersionConvention()); //ApiExplorerGroupPerVersionConvention.cs used by SwashBuckle

                })
                .AddXmlSerializerFormatters()
                .AddXmlDataContractSerializerFormatters()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            string xmlPath;
            services.AddSwaggerGen(c =>
            {

                c.SwaggerDoc("v1", new OpenApiInfo { Title = "EPD V1", Version = "v1", Description = "EPD API gebruikt door de SOAP layer" });
                c.SwaggerDoc("v2", new OpenApiInfo { Title = "EPD V2", Version = "v2", Description = "EPD Api met JWT Tokens en TokenRefresh" });

                c.DescribeAllEnumsAsStrings();
                c.DescribeStringEnumsInCamelCase();

                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                 xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                c.IncludeXmlComments(xmlPath);
            });

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {

            app.UseAuthentication();
            app.UseExceptionHandler("/globalerr/{0}");

            //Register Syncfusion license
            Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(Configuration["Syncfusion:license"]);

            if (!env.IsDevelopment())
            {
                app.UseHsts(new HstsOptions(TimeSpan.FromDays(366), includeSubDomains: false, preload: false));
                var options = new RewriteOptions()
                    .AddRedirectToHttpsPermanent();

                app.UseRewriter(options);
            }
            else
            {
                app.UseHttpsRedirection();
            }

            //https://securityheaders.com :
            app.UseCsp(csp =>
            {  //https://github.com/juunas11/aspnetcore-security-headers

                // Allow JavaScript from:
                csp.AllowScripts
                    .FromSelf() //This domain
                    .From("www.googletagmanager.com")
                    .From("www.google-analytics.com")
                    .From("embed.tawk.to")
                    .From("cdn.jsdelivr.net")
                    .From("code.jquery.com")
                    .From("www.islonline.com");

                // CSS allowed from:
                csp.AllowStyles
                    .FromSelf()
                    .From("cdn.jsdelivr.net")
                    .From("fonts.googleapis.com")
                    .From("maxcdn.bootstrapcdn.com")
                    .AllowUnsafeInline();
                csp.AllowFonts
                    .From("fonts.gstatic.com")
                    .From("static-v.tawk.to");
                csp.AllowFrames
                    .From("va.tawk.to");

                csp.OnSendingHeader = context =>
                {
                    context.ShouldNotSend = context.HttpContext.Request.Path.StartsWithSegments("/api") || context.HttpContext.Request.Path.StartsWithSegments("/css")
                                                                                                        || context.HttpContext.Request.Path.StartsWithSegments("/js");
                    return Task.CompletedTask;
                };

            });
            app.Use(async (context, next) =>
            {

                //set standard security headers
                context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
                context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
                context.Response.Headers.Append("X-Frame-Options", "ALLOW-FROM datec.nl va.tawk.to");

                context.Response.Headers.Remove("x-powered-by");  //doet het niet

                context.Response.Headers.Add(
                    "Referrer-Policy", "strict-origin-when-cross-origin"); //only send referrers when going to our own site on https
                context.Response.Headers.Add(
                    "Feature-Policy", "geolocation 'none'; camera 'none'; microphone 'none' "); //we do not need features
                await next.Invoke();
            });
            if (Configuration["AppSettings:UseWebMarkupMin"] == "true")
            {
                app.UseWebMarkupMin();
            }

            app.UseStaticFiles(new StaticFileOptions
            {

                OnPrepareResponse = context =>
                {
                    const int durationInSeconds = 15778463; // 6 months //31556926; //1 year
                    context.Context.Response.Headers.Append("Cache-Control", $"public, max-age={durationInSeconds}");
                }
            });

            app.UseCookiePolicy();

            app.UseMvc(routes =>
            {

                routes.MapRoute(
                    "online",
                    "{area:exists}/{action=Index}",
                    new { controller = "Home" });
                routes.MapRoute(
                     "areas",
                     "{area:exists}/{controller}/{action=Index}");
                routes.MapRoute(
                     "base",
                    "{action}",
                    new { controller = "Base", action = "Index" });

            });

            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v2/swagger.json", "Testmij api V2");

                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Testmij api V1");

            });

            app.UseSignalR(route =>
            {
                route.MapHub<StatusHub>("/statushub");
            });
            try
            {
                StartupDataProtection instance = ActivatorUtilities.CreateInstance<StartupDataProtection>(app.ApplicationServices);  
                //added as recommended here: https://github.com/aspnet/DataProtection/issues/233
                instance.Init();
            }
            catch (Exception ex)
            {
                Logger logger = LogManager.GetCurrentClassLogger();
                logger.Fatal(ex);

            }
        }
    }
}
blowdart commented 5 years ago

@vankampenp Did you try setting a static application name? I want to rule out the easy stuff first

    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(Config.Settings.DropDirectory));
        .SetApplicationName("MyStaticAppName");

You shouldn't need StartupDataProtection instance = ActivatorUtilities.CreateInstance<StartupDataProtection>(app.ApplicationServices); any more unless you're on 1.x which went out of support this month. If that's the case please consider upgrading to 2.1 or 2.2 as quickly as you can.

vankampenp commented 5 years ago

Thanks Barry. I am on 2.2. (2.2.6 after the security advisory yesterday). I will give that a try. The part was inserted in 1.1 after we had some issues. I tried removing it later, but it still occurred. I will report back whether the static application name helps eliminating the GeneratePasswordResetToken issue.

vankampenp commented 5 years ago

@blowdart I removed the CreateInstance, and just added the code above in ConfigureServices, with my own application name. I posted identical code to the two sites. Unfortunately, the issue still occurs, the token is not recognized when created on site A, for use on site B.

blowdart commented 5 years ago

This is so weird. OK, we'll investigate some more.

vankampenp commented 5 years ago

This is the code for admin to reset a user password:

   ```
   /// <summary>
    /// Request to reset the password of a user
    /// </summary>
    /// <param name="examiner"></param>
    [HttpPost]
    public async Task<string[]> AdminResetPwd([FromBody] DtoExaminer examiner)
    {

        DtoUser user = await _userManager.FindByNameAsync(examiner.Examiner);
        await _userManager.AddToRoleAsync(user, DtoRoles.Examiner);
        string site = Url.Action("", "home", new {Area = "online"}, HttpContext.Request.Scheme);
        var token = await _userManager.GeneratePasswordResetTokenAsync(user);

        var err = await UserMails.SendResetEmail(token,site,user.LanguageId,user.DisplayName,user.Email);
        if (err != "") return new[] { "messagebad", err };
        TempCookie("MessageGood", "Een reset email is gestuurd");
        return new[] { "/online/admin?tab=1" };
    }

And this is where the link points to:
    /// <summary>
    /// called from the link in the email, 
    /// first verify if the token is still valid
    /// if so redirect to Reset
    /// </summary>
    /// <param name="token"></param>
    /// <param name="email"></param>
    [AllowAnonymous]
    [HttpGet]
    public async Task<IActionResult> ResetToken([FromQuery] string token, [FromQuery] string email)
    { 
        if (string.IsNullOrEmpty(email))
        {
            Logger.Warn("No user email for token request ");
            return Redirect(Url.Action(nameof(ForgotPassword).ToLower(), "account", new { Area = "online", isInvalidToken = true }, HttpContext.Request.Scheme)); //try another time
        }

        email = email.Clean();
        Logger.Warn("User {0} is resetting the password from the token ", email);
        try
        {
            DtoUser user = await _userManager.FindByEmailAsync(email);
            if (user == null)
            {
                Logger.Warn("Invalid user email for token request '{0}'", email);
                return Redirect(Url.Action(nameof(ForgotPassword).ToLower(), "account", new { Area = "online", isInvalidToken = true }, HttpContext.Request.Scheme)); //try another time
            }
            var userAgent = Request.Headers["User-Agent"].ToString();
            if (string.IsNullOrEmpty(userAgent)) Logger.Error("User-Agent empty");

            var ua = new UserAgent(userAgent ?? "");
            var userDetails = new
            {
                User = email,
                Host = HttpContext.Request.Host,
                Browser = ua.Browser.Name,
                BrowserVersion = ua.Browser.Version,
                OS = ua.Os.Name,
                OSversion = ua.Os.Version,
                siteVersion = Config.Settings.DtoVersion
            };
            bool success = await _userManager.VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, "ResetPassword", token);
            if (!success)
            {
                Logger.Warn("Invalid Token receved for '{0}'", userDetails);
                return Redirect(Url.Action(nameof(ForgotPassword).ToLower(), "account", new { Area = "online", isInvalidToken = true }, HttpContext.Request.Scheme)); //try another time
            }

            Logger.Trace("Received Token for {0}", userDetails);
        }
        catch (Exception e)
        {
            Logger.Error(e);

            return LocalRedirect(Url.Action(nameof(HomeController.Index).ToLower(), "home", new { Area = "online", message = _siteLanguage.UnknownError, messageIsGood = false }));
        }
        return LocalRedirect(Url.Action(nameof(Reset).ToLower(), "account", new
        {
            Area = "online",
            message = _siteLanguage.AccountRedirect,
            messageIsGood = true,
            token,
            email
        }));
    }


userManager.VerifyUserTokenAsync returns false
blowdart commented 5 years ago

Can you slim this down to the smallest repo possible for Hao to look at?

vankampenp commented 5 years ago

@blowdart I will try to do this next week

vankampenp commented 5 years ago

@HaoK Attached the repo as promised. This repo demonstrates the issue that when using a different domain url, the token no longer works.

The demo simulates the process where a user registers an account, and is sent a token via email that is generated through userManager.GeneratePasswordResetTokenAsync(user). In stead of mailing the link, the link is just displayed on the site. After clicking the link, the token is verified against the user.

Launch the project using Kestrel server. Configure the host address that you use in application.json Host. Default is https://localhost:5001. On the first launch execute the migrations to populate the Identity database.

Step 1: click on Register, and enter an email address (it is not used to email, just as the user name and registered email address)

Step 2: The link is displayed (as if you are in your email app). Click the link.

Step 3: your token is verified, if correct, you can now set your password (Upper/Lower case, numeric and non-alphanumeric character). Press Set Password. You are now logged in.

You can change the password by clicking on your email address in the home page, or by logging out and clicking on Register again.

Step 4: Stop the app, and now copy the repo in to a new folder. Do not change anything. Start the app with a different port, for example by starting it using IIS Express this time. If needed, you can change the port number in this second repo.

Step 5: start the original repo and the copied repo. Both have the Host configured to point to the first repo executable.

Step 6: In the second repo login with your registered email address. Note that this is possible because both repo's use the same underlying data store.

Step 7: In the second repo click on your email address (Forgot Password), to reset your password.

Step 8: You get a link with the generated token, pointing to the url of the first repo. Click it.

Step 9: Notice you are redirected to the first repo, your token is checked, but it returns an error. The app shows the message "Invalid Token".

GeneratePasswordResetTokenAsync1.zip

HaoK commented 4 years ago

@blowdart can you confirm what's the default behavior of dataprotection? This app isn't doing any configuration of dataprotection, so I'm assuming the issue is that copying the app results in differences in the two apps dataprotection keys since they aren't configured to share the same keys.

blowdart commented 4 years ago

Apps are isolated by default, even if they share the keyring.

HaoK commented 4 years ago

Okay so everything is working by design here then.

blowdart commented 4 years ago

@vankampenp If these are indeed separate instances on separate domain names you need to configure the application name to be a fixed value in all instances;

public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .SetApplicationName("example");
}
vankampenp commented 4 years ago

Thanks @blowdart, After making the sample, I found that. But still no luck. But by also persisting the keys to the same UNC location did the trick. services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Configuration["AppSettings:keydir"])) .SetApplicationName(Configuration["AppSettings:keyname"]);

This now works!