Views: 1015
Number of votes: 2
Average rating:

Curing your Redirect woes when using ASP.NET Identity and UISignInManager.SignIn()

Hello World.

In this post I'll show you how to modify UISignInManager so that it does not fully rely on the ReturnUrl query parameter for redirects, enabling us to use whatever redirection logic we may desire and/or require!

The context

I recently ran into a thread here on World where a fellow developer had some issues with users being redirected to the sites' root page (”/”) after signing them in through a call to UISignInManager.SignIn().

The intent was to redirect the users to a specific Url after signing them in. Sounds reasonable.

But the issue with redirecting to another url after calling SignIn() is that the SignIn method already sets a redirect location on the response, which means that you can try to redirect as much as you want afterwards. The user will still be redirected to whatever the SignIn() method decided to redirect to.

And what does the SignIn()-method decide to redirect to? The ReturnUrl in the query parameters (for example ”/Login?ReturnUrl=/ALockedPage”).

This is fine in most cases where you, for example, get redirected to the login page for trying to access a page where users need to be authenticated. But what happens if you don’t have a ReturnUrl query paremeter set? Well it will simply redirect to ”/” – in other words, the sites’ root page. This may not be what you want in all cases, especially if you have a custom login page with custom redirection logic.

Note: This SignIn() redirect also overrides redirects that you do in the OnResponseSignIn Action in your OWIN configuration (a.k.a. Startup.cs).

So how do we solve this?

Well, we create our own UISignInManager of course!

In this approach we'll modify the SignIn method so that it doesn't automatically redirect to sites' root page if we do not have a ReturnUrl set.

    public class MyApplicationUISignInManager<TUser> : ApplicationUISignInManager<TUser> where TUser : IdentityUser, IUIUser, new()
    {

        protected ApplicationSignInManager<TUser> _signInManager;

        public MyApplicationUISignInManager(ServiceAccessor<ApplicationSignInManager<TUser>> signInManager) : base(signInManager)
        {
            _signInManager = signInManager();
        }

        public override bool SignIn(string providerName, string userName, string password)
        {

            if (HasReturnUrl()) // Use the default implementation if we have a ReturnUrl set
                return base.SignIn(providerName, userName, password);

            return _signInManager.SignIn(userName, password, null);
        }

        private bool HasReturnUrl()
        {
            return HttpContext.Current?.Request.QueryString?.Get("ReturnUrl") != null;   
        }


        /// <summary>
        /// Creates an instance of MyApplicationUISignInManager. Mainly for setting up the IoC Container.
        /// </summary>
        /// <param name="options"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public new static UISignInManager Create(IdentityFactoryOptions<UISignInManager> options, IOwinContext context)
        {
            return new MyApplicationUISignInManager<TUser>(context.Get<ApplicationSignInManager<TUser>>);
        }
    }

In this case, we only want to modify what happens if we do not have a ReturnUrl. So if we have a query parameter named ReturnUrl, we just use the standard UISignInManager code to handle that case. Otherwise we sign the user in ourselves without specifying a return url.

Now we need to setup our IoC to use our new UISignInManager.

This can be done in your Startup.cs file, just after you call the built in AddCmsAspNetIdentity() where all the default Managers and Providers are setup.

    // Example Startup.cs class
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.AddCmsAspNetIdentity<ApplicationUser>();

            app.CreatePerOwinContext<UISignInManager>(MyApplicationUISignInManager<ApplicationUser>.Create); // <-- Add this line after calling app.AddCmsAspNetIdentity

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Util/Login.aspx"),
                Provider = new CookieAuthenticationProvider
                {
                    OnValidateIdentity =
                        SecurityStampValidator
                            .OnValidateIdentity<ApplicationUserManager<User>, User>(
                                TimeSpan.FromMinutes(30),
                                (manager, user) => manager.GenerateUserIdentityAsync(user)),
                    OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri),
                    OnResponseSignOut = context => context.Response.Redirect("/ByeWorld"),
                    OnResponseSignIn = context => context.Response.Redirect("/HelloWorld") // This now works too! Yay!
                }
            });
        }
    }

Note: In my example I call a static Create method that lives in my own implementation of UISignInManager, this is so that it’s done in a similar fashion to how the others defaults are setup in case you set them up manually instead of calling AddCmsAspNetIdentity().

Now you’re set and can start redirecting to whatever you want, either through the OnResponseSignIn action or manually after signing in users through UISignInManager.SignIn()! 😊

Sep 20, 2018

Please login to comment.