ritterim / stuntman

Library for impersonating users during development leveraging ASP.NET Identity.
https://rimdev.io/stuntman/
MIT License
294 stars 35 forks source link

Only activate stuntman after "real" authentication. #176

Closed n4ppy closed 6 years ago

n4ppy commented 6 years ago

Hi,

I have stuntman installed and works great for dev but i would also like to use it in our demo/test environment on the web (open for basically anyone).

Issue now is that anyone can just pick a user and get in but i would like the original owin authentication to pop up first and after that be able to use stuntman to quickly demo/test.

Thanks

Franc

khalidabuhakmeh commented 6 years ago

This is a matter of ordering the authentication. Stuntman also has a pass through mode. I think @billbogaiv can help you :)

billbogaiv commented 6 years ago

In something like Startup.cs, just make sure the UseStuntman middleware is after the primary identity middleware:

// Use primary authentication system first...
app.UseIdentityServer(...);

// Then, Stuntman...
app.UseStuntman(...);

If you are using ASP.NET, in your controller-action, the direction you take is up to your individual needs:

// Get Stuntman identity...
var stuntmanIdentity = (User as ClaimsPrincipal).Identities.Where(x => x.AuthenticationType == RimDev.Stuntman.Core.Constants.StuntmanAuthenticationType);

Assuming your entire app. requires authentication, this flow should work fine. If you have a landing-page that allows anonymous users, then it will take a bit more work (since you don't want to show the Stuntman user-picker prior to using the primary authentication service):

_Layout.cshtml

@if (User.IsAuthenticated)
{
    @Html.Raw(YourApplicationNamespace.Startup.StuntmanOptions.UserPicker(User));
}
n4ppy commented 6 years ago

Basically monkey see monkey do for me :)

If i put Stuntman at the top, stuntman does not work, if i put stuntman last i go to stuntman/sign-in not /Account/Login where i would like to go first. Probably a noob setting somewhere :)

Thanks Franc

        {
            // Configure the db context, user manager and signin manager to use a single instance per request
            app.CreatePerOwinContext(ApplicationDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
            app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

            // Enable the application to use a cookie to store information for the signed in user
            // and to use a cookie to temporarily store information about a user logging in with a third party login provider
            // Configure the sign in cookie
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    // Enables the application to validate the security stamp when the user logs in.
                    // This is a security feature which is used when you change a password or add an external login to your account.  
                    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                        validateInterval: TimeSpan.FromMinutes(30),
                        regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
                },
                CookieDomain = Settings.Default.CookieDomain, 
            });
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
            app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

            // Enables the application to remember the second login verification factor such as phone or email.
            // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
            // This is similar to the RememberMe option when you log in.
            app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

            // https://github.com/ritterim/stuntman
            if (!string.IsNullOrWhiteSpace(Settings.Default.StuntManEnabledFor))
            {
                StuntmanOptions.SetUserPickerAlignment(StuntmanAlignment.Right);
                StuntmanOptions.AddUsers(StuntmanHelper.StuntManUsers(Settings.Default.StuntManEnabledFor));
                StuntmanOptions.AllowBearerTokenPassthrough = true;

                if (System.Web.HttpContext.Current.IsDebuggingEnabled)
                {
                    app.UseStuntman(StuntmanOptions);
                }
            }
billbogaiv commented 6 years ago

Alternatively, you could also try in ASP.NET Core:

app.UseWhen(context =>
            context.Request.User.Identity.IsAuthenticated, appBuilder =>
{
    appBuiler.UseStuntman(...);
});

If using classic ASP.NET, you'll need this extension:

using Microsoft.Owin.Builder;
using Owin;
using System;
using Predicate = System.Func<Microsoft.Owin.IOwinContext, bool>;

namespace Extensions
{
    public static class IAppBuilderExtensions
    {
        /// <summary>
        /// Conditionally creates a branch in the request pipeline that is rejoined to the main pipeline.
        /// Modified from https://github.com/aspnet/HttpAbstractions/blob/01f247db4854fd066fb8567a599a4bb591cb392b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UseWhenExtensions.cs.
        /// </summary>
        /// <param name="predicate">Invoked with the request environment to determine if the branch should be taken</param>
        /// <param name="configuration">Configures a branch to take</param>
        /// <returns></returns>
        public static IAppBuilder UseWhen(this IAppBuilder app, Predicate predicate, Action<IAppBuilder> configuration)
        {
            app = app ?? throw new ArgumentNullException(nameof(app));
            predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
            configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));

            return app.Use((context, next) =>
            {
                // Not sure why this next section has to be within the `Use` block. Original was outside. Otherwise, MiniProfiler usage throws Exception.
                var builder = app.New();
                configuration(builder);

                builder.Run(_ => next());
                var branch = builder.Build();

                if (predicate(context))
                {
                    return branch(context.Environment);
                }
                else
                {
                    return next();
                }
            });
        }
    }
}

What this should do is only execute Stuntman middleware if the User is already authenticated with your primary authentication middleware. I have never tried this exact setup before, but seems like it would work.

n4ppy commented 6 years ago

Some small tweaks

       if (app == null) { throw new ArgumentNullException(nameof(app)); }
        if (predicate == null) { throw new ArgumentNullException(nameof(predicate)); }
        if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); }

and

app.UseWhen(context => context.Request.User.Identity.IsAuthenticated, appBuilder => { appBuilder.UseStuntman(StuntmanOptions); });

Seems to work but need to do some more testing (will be early next week as it's end of day for me)

Thanks so far!

n4ppy commented 6 years ago
            if (!string.IsNullOrWhiteSpace(Settings.Default.StuntManEnabledFor))
            {
                StuntmanOptions.SetUserPickerAlignment(StuntmanAlignment.Right);
                StuntmanOptions.AddUsers(StuntmanHelper.StuntManUsers(Settings.Default.StuntManEnabledFor));

                if (System.Web.HttpContext.Current.IsDebuggingEnabled)
                {
                    app.UseWhen(context => context.Request.User.Identity.IsAuthenticated, appBuilder => { appBuilder.UseStuntman(StuntmanOptions); });
                }
            }

have tested it in the demo environment and this does the trick when authenticated the stuntman option is available otherwise it's not.

(StuntmanHelper.StuntManUsers is a method to return a specific list of users)

So thanks, works great and made life easier for testing and doing a demo with some screens having 3 different roles looking at it and getting different buttons depending on role