blowdart / idunno.CookieSharing

Project demonstrating how to share authentication cookies between ASP.NET 4.5 and ASP.NET Core applications.
77 stars 27 forks source link

Cookie sharing with redis. #9

Open seriouz opened 7 years ago

seriouz commented 7 years ago

I don't see how it is possible to allow cookie sharing over redis in 1.1. Is there a RedisDataProtectionProvider? The root of my problem lays in an enviroment, where the published dotnet folder is copied into a dynamic folder (/opt/deploy/number-{1-n}). Every time the folder with the app changes, all tokens of the system are invalid (authentication cookies, email-tokens etc). I tried to use Redis but this did not work either and all tokens were claimed invalid.


using SharedApp.Config;
using SharedApp.Data;
using SharedApp.Models;
using SharedApp.NoSQL;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using Orchard.ResourceManagement;
using System;
using System.IO;
using System.Linq;

namespace SharedApp
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.custom.json", optional: true);

            if (env.IsDevelopment())
            {
                // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets();
            }

            builder.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)
        {
            services.AddSingleton<IConfigurationRoot>(Configuration);

            // sad but a giant hack :(
            // https://github.com/StackExchange/StackExchange.Redis/issues/410#issuecomment-220829614
            //var redis = ConnectionMultiplexer.Connect($"127.0.0.1:6379");

            //services.AddDataProtection().PersistKeysToRedis(redis, "DataProtection-Keys");

            services.AddOptions();

            // Add Memory Cache
            //services.AddMemoryCache();

            // Add framework services.
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            /* Configure identity tokens */
            //services.Configure<IdentityOptions>(options =>
            //{
            //    options.Cookies.ApplicationCookie.AuthenticationScheme = "ApplicationCookie";
            //    options.Tokens.ChangeEmailTokenProvider = "Default";
            //    options.Tokens.EmailConfirmationTokenProvider = "Default";
            //    options.Tokens.PasswordResetTokenProvider = "Default";

            //    options.Cookies.ApplicationCookie.LoginPath = new PathString("/Index");
            //    options.Cookies.ApplicationCookie.LogoutPath = new PathString("/Index");
            //    // options.Cookies.ApplicationCookie.AccessDeniedPath = new PathString("/AccessDenied");

            //    options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
            //    options.Cookies.ApplicationCookie.AutomaticChallenge = true;
            //    options.Cookies.ApplicationCookie.CookieName = ".AspNet.SharedCookie";
            //});
            services.Configure<Microsoft.AspNetCore.Identity.DataProtectionTokenProviderOptions>(options =>
            {
                options.TokenLifespan = TimeSpan.FromDays(7);
            });

            services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            {
                options.Cookies = new Microsoft.AspNetCore.Identity.IdentityCookieOptions
                {
                    ApplicationCookie = new CookieAuthenticationOptions
                    {
                        AuthenticationScheme = "ApplicationCookie",
                        LoginPath = new PathString("/Index"),
                        LogoutPath = new PathString("/Index"),
                        AccessDeniedPath = new PathString("/Index"),
                        AutomaticAuthenticate = true,
                        AutomaticChallenge = true,
                        CookieName = ".AspNet.SharedCookie"

                    }
                };

                options.Tokens.ChangeEmailTokenProvider = "Default";
                options.Tokens.EmailConfirmationTokenProvider = "Default";
                options.Tokens.PasswordResetTokenProvider = "Default";
            })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider,
                SharedApp_Prototpye.Mvc.DataAnnotations.Internal.ValidationAttributeAdapterProvider>();
            services.AddMvc(config =>
            {
                // Makes it possible that one function in a controller
                // can send a json respose or a text/application (view)
                // response.
                config.RespectBrowserAcceptHeader = true; // false by default
                config.Conventions.Add(new Tools.ApiExplorerAreaApiOnlyConvention());
            }).AddJsonOptions(options => {
                // handle loops correctly
                options.SerializerSettings.ReferenceLoopHandling =
                    Newtonsoft.Json.ReferenceLoopHandling.Ignore;

                // use standard name conversion of properties
                options.SerializerSettings.ContractResolver =
                    new DefaultContractResolver();

                // include $id property in the output
                //options.SerializerSettings.PreserveReferencesHandling =
                //    PreserveReferencesHandling.Objects;
            });

            // Custom route contraint project.
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("project", typeof(Routing.ProjectRouteConstraint))
            );

            //services.AddAuthorization(options =>
            //{
            //    options.AddPolicy("Authorize", policy =>
            //    {
            //        policy.AddRequirements(new Authorization.IsAdminRequirement());
            //    });
            //});

            //services.Configure<IdentityOptions>(options =>
            //{
            //    options.Cookies.ApplicationCookie.LoginPath = new PathString("/Index");
            //    options.Cookies.ApplicationCookie.LogoutPath = new PathString("/Index");
            //    // options.Cookies.ApplicationCookie.AccessDeniedPath = new PathString("/AccessDenied");
            //});

            // Additional aspnet services.
            services.AddSingleton<Microsoft.AspNetCore.Mvc.Infrastructure.IActionContextAccessor, Microsoft.AspNetCore.Mvc.Infrastructure.ActionContextAccessor>();

            services.AddOptions();
        }

        // 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, ApplicationDbContext dbContext)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Hosting enviroment: " + env.EnvironmentName);
            Console.ResetColor();

            if (env.IsDevelopment())
            {
                loggerFactory.AddDebug(LogLevel.Debug);
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseBrowserLink();
            }
            else
            {
                loggerFactory.AddDebug(LogLevel.Information);
            }

            app.UseStaticFiles();

            var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@"c:\tokensTest"));
            var dataProtector = protectionProvider.CreateProtector(
                "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                "Cookie",
                "v2");
            var ticketFormat = new Microsoft.AspNetCore.Authentication.TicketDataFormat(dataProtector);
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationScheme = "Cookie",
                CookieName = ".AspNet.SharedCookie",
                TicketDataFormat = ticketFormat
            });

            app.UseIdentity();

            app.UseMvc();
        }
    }
}
blowdart commented 7 years ago

There is a redis provider, it popped up in 1.1; https://github.com/aspnet/DataProtection/tree/dev/src/Microsoft.AspNetCore.DataProtection.Redis

seriouz commented 7 years ago

I created a new web project (vs2015) with identity. When i follow the guidance of your link it does not work either. I think the problem is, that i cannot set a TicketDataFormat when i use PersistKeysToRedis.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedCookieRedis.Data;
using SharedCookieRedis.Models;
using SharedCookieRedis.Services;
using StackExchange.Redis;
using Microsoft.AspNetCore.DataProtection;

namespace SharedCookieRedis
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

            if (env.IsDevelopment())
            {
                // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets();
            }

            builder.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)
        {
            // Add framework services.
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            var redis = ConnectionMultiplexer.Connect("localhost:6379");
            services.AddDataProtection().PersistKeysToRedis(redis, "DataProtection-Keys");

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();

            // Add application services.
            services.AddTransient<IEmailSender, AuthMessageSender>();
            services.AddTransient<ISmsSender, AuthMessageSender>();
        }

        // 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)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseIdentity();

            // Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}
blowdart commented 7 years ago

Ah, that might be right. The cookie is supposed to be self contained. I don't think we ever looked at using redis on both sides to store the identity, and then issue a reference cookie. The data protection provider for redis is just for key storage.

seriouz commented 7 years ago

Thanks for your reply! So "cookie sharing" is not working out of the box currently with redis (https://github.com/blowdart/idunno.CookieSharing Article 3)? I am now using DataProtectionProvider with a folder, which works flawlessly.

Sharing working:


        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@"c:\testKeyFolder"));
            var dataProtector = protectionProvider.CreateProtector(
                "CookieAuthenticationMiddleware", "Cookie", "v2");
            var ticketFormat = new TicketDataFormat(dataProtector);

            services.AddIdentity<ApplicationUser, IdentityRole>(options =>
                {
                    options.Cookies = new Microsoft.AspNetCore.Identity.IdentityCookieOptions
                    {
                        ApplicationCookie = new CookieAuthenticationOptions
                        {
                            AuthenticationScheme = "Cookie",
                            LoginPath = new PathString("/Account/Login/"),
                            AccessDeniedPath = new PathString("/Account/Forbidden/"),
                            AutomaticAuthenticate = true,
                            AutomaticChallenge = true,
                            CookieName = ".AspNet.SharedCookie",
                            TicketDataFormat = ticketFormat
                        }
                    };
                })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
blowdart commented 7 years ago

The cookie sharing I'm referring to is for logins/authentication, in it's basic form, where the cookie has all the user information, nothing more. Not sessions, not reference cookies where the identity is elsewhere, because, honestly that covers the 99% of what folks want.

blowdart commented 7 years ago

Ah I see, if it works with a folder it ought to work with redis as the key store. @pakrym do you think you can make this work?