How to force re authentication between ASP Net Cor

2019-08-24 06:32发布

问题:

I have an ASP.Net Core MVC web application which uses Azure AD for authentication. I have just received a new requirement to force user to reauthenticate before entering some sensitive information (the button to enter this new information calls a controller action that initialises a new view model and returns a partial view into a bootstrap modal).

I have followed this article which provides a great guide for achieving this very requirement. I had to make some tweaks to get it to work with ASP.Net Core 2.0 which I think is right however my problems are as follows...

  1. Adding the resource filter decoration "[RequireReauthentication(0)]" to my controller action works however passing the value 0 means the code never reaches the await.next() command inside the filter. If i change the parameter value to say 30 it works but seems very arbitrary. What should this value be?

  2. The reauthentication works when calling a controller action that returns a full view. However when I call the action from an ajax request which returns a partial into a bootstrap modal it fails before loading the modal with

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://localhost:44308' is therefore not allowed access

This looks like a CORS issue but I don't know why it would work when going through the standard mvc process and not when being called from jquery. Adding

services.AddCors();

app.UseCors(builder => builder.WithOrigins("https://login.microsoftonline.com"));

to my startup file doesn't make any difference. What could be the issue here?

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // Ommitted for clarity...

    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddAzureAd(options => Configuration.Bind("AzureAd", options))
    .AddCookie();

    services.AddCors();

    // Ommitted for clarity...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Ommitted for clarity...

    app.UseCors(builder => builder.WithOrigins("https://login.microsoftonline.com"));

    app.UseStaticFiles();

    app.UseAuthentication();

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

AzureAdAuthenticationBuilderExtensions.cs

public static class AzureAdAuthenticationBuilderExtensions
{        
    public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
        => builder.AddAzureAd(_ => { });

    public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
    {
        builder.Services.Configure(configureOptions);
        builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
        builder.AddOpenIdConnect(options =>
        {
            options.ClaimActions.Remove("auth_time");
            options.Events = new OpenIdConnectEvents
            {
                OnRedirectToIdentityProvider = RedirectToIdentityProvider
            };
        });
        return builder;
    }

    private static Task RedirectToIdentityProvider(RedirectContext context)
    {
        // Force reauthentication for sensitive data if required
        if (context.ShouldReauthenticate())
        {
            context.ProtocolMessage.MaxAge = "0"; // <time since last authentication or 0>;
        }
        else
        {
            context.Properties.RedirectUri = new PathString("/Account/SignedIn");
        }

        return Task.FromResult(0);
    }

    internal static bool ShouldReauthenticate(this RedirectContext context)
    {
        context.Properties.Items.TryGetValue("reauthenticate", out string reauthenticate);
        bool shouldReauthenticate = false;

        if (reauthenticate != null && !bool.TryParse(reauthenticate, out shouldReauthenticate))
        {
            throw new InvalidOperationException($"'{reauthenticate}' is an invalid boolean value");
        }

        return shouldReauthenticate;
    }

    // Ommitted for clarity...
}

RequireReauthenticationAttribute.cs

public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
    private int _timeElapsedSinceLast;
    public RequireReauthenticationAttribute(int timeElapsedSinceLast)
    {
        _timeElapsedSinceLast = timeElapsedSinceLast;
    }
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst("auth_time")?.Value, out int authTime);
        var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();

        if (foundAuthTime && ts - authTime < _timeElapsedSinceLast)
        {
            await next();
        }
        else
        {
            var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
            await AuthenticationHttpContextExtensions.ChallengeAsync(context.HttpContext, OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state));
        }
    }
}

CreateNote.cs

[HttpGet]
[RequireReauthentication(0)]
public IActionResult CreateNote(int id)
{
    TempData["IsCreate"] = true;
    ViewData["PostAction"] = "CreateNote";
    ViewData["PostRouteId"] = id;
    var model = new NoteViewModel
    {
        ClientId = id
    };
    return PartialView("_Note", model);
}

Razor View (snippet)

<a asp-controller="Client" asp-action="CreateNote" asp-route-id="@ViewData["ClientId"]" id="client-note-get" data-ajax="true" data-ajax-method="get" data-ajax-update="#client-note-modal-content" data-ajax-mode="replace" data-ajax-success="ShowModal('#client-note-modal', null, null);" data-ajax-failure="AjaxFailure(xhr, status, error, false);"></a>

All help appreciated. Thanks

回答1:

The CORS problem is not in your app. Your AJAX call is trying to follow the authentication redirect to Azure AD, which will not work.

What you can do instead is in your RedirectToIdentityProvider function, check if the request is an AJAX request. If it is, make it return a 401 status code, no redirect.

Then your client-side JS needs to detect the status code, and issue a redirect that triggers the authentication.