如何在ASP Net Core 2.0 MVC Web应用程序和Azure AD之间强制重新身份验证 [英] How to force re authentication between ASP Net Core 2.0 MVC web app and Azure AD

本文介绍了如何在ASP Net Core 2.0 MVC Web应用程序和Azure AD之间强制重新身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个使用Azure AD进行身份验证的ASP.Net Core MVC Web应用程序.我刚收到一个新要求,即在输入一些敏感信息之前强制用户重新进行身份验证(输入此新信息的按钮将调用控制器动作,该动作将初始化新视图模型并将部分视图返回到引导模式).

我关注了这篇文章,为实现此要求提供了很好的指南.我必须进行一些调整才能使其与ASP.Net Core 2.0一起使用,我认为这是正确的,但是我的问题如下...

  1. 向我的控制器操作添加资源过滤器修饰"[RequireReauthentication(0)]"是可行的,但是传递值0意味着代码永远不会到达过滤器内的await.next()命令.如果我将参数值更改为30,则可以正常工作,但似乎很随意.这个值应该是什么?

  2. 当调用返回完整视图的控制器操作时,重新认证有效.但是,当我从ajax请求中调用操作,该操作将部分返回到引导模式时,在用

  3. 加载模式之前,它会失败

对预检请求的响应未通过访问控制检查:否 请求中存在"Access-Control-Allow-Origin"标头 资源.因此,不允许使用来源' https://localhost:44308 访问

这看起来像是一个CORS问题,但我不知道为什么在通过标准mvc进程而不是从jquery调用它时它将起作用.添加

services.AddCors();

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

对我的启动文件没有任何影响.这里可能是什么问题?

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);
}

剃刀视图(摘要)

<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>

感谢所有帮助.谢谢

解决方案

您的应用程序中没有CORS问题. 您的AJAX呼叫正尝试遵循身份验证重定向到Azure AD, 这将不起作用.

您可以做的是在您的RedirectToIdentityProvider函数中,检查该请求是否为AJAX请求. 如果是这样,则使其返回401状态代码,而不进行重定向.

然后,您的客户端JS需要检测状态代码,并发出触发身份验证的重定向.

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

解决方案

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.

这篇关于如何在ASP Net Core 2.0 MVC Web应用程序和Azure AD之间强制重新身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆