每个路由使用不同的身份验证架构(Windows,承载)

时间:2018-10-26 16:40:31

标签: iis asp.net-core windows-authentication asp.net-core-2.1

我需要使用Windows身份验证将单点登录添加到我的Intranet Angular Web应用程序(在IIS上托管),该应用程序使用JWT Bearer令牌进行身份验证。使用[Authorize]属性保护控制器,并且JWT Bearer令牌认证正在运行。所有控制器都在api/路由下公开。

想法是在SsoController路由下发布新的sso/,该路由应通过Windows身份验证进行保护,并公开一个WindowsLogin操作,该操作返回应用程序的有效承载令牌

当我使用ASP.net Web窗体时,这非常容易,您只需要在web.config/system.webServer部分中启用Windows身份验证,在system.web部分中在整个应用程序中将其禁用,然后启用再次在<location path="sso">标签下。这样,ASP.net仅针对sso路由下的请求生成NTLM /协商挑战。

我几乎可以正常工作-SsoController获得了Windows用户名并创建了JWT令牌,但是管道仍在为 all 生成WWW-Authenticate: NTLMWWW-Authenticate: Negotiate标头 HTTP 401响应,而不仅仅是针对sso路由下的响应。

如何告诉管道,我只希望对所有api/请求进行匿名或不记名身份验证?

预先感谢您的帮助。

Program.cs

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
  WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
    .UseIISIntegration();

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // Set up data directory
    services.AddDbContext<AuthContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("AuthContext")));

    services.AddAuthentication(IISDefaults.AuthenticationScheme);
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,

                ValidIssuer = "AngularWebApp.Web",
                ValidAudience = "AngularWebApp.Web.Client",
                IssuerSigningKey = _signingKey,
                ClockSkew = TimeSpan.Zero   //the default for this setting is 5 minutes
            };
            options.Events = new JwtBearerEvents
            {
                OnAuthenticationFailed = context =>
                {
                    if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                    {
                        context.Response.Headers.Add("Token-Expired", "true");
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    // In production, the Angular files will be served from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseSpaStaticFiles();
    app.UseAuthentication();

    app.UseWhen(context => context.Request.Path.StartsWithSegments("/sso"),
        builder => builder.UseMiddleware<WindowsAuthMiddleware>());

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

    app.UseSpa(spa =>
    {
        // To learn more about options for serving an Angular SPA from ASP.NET Core,
        // see https://go.microsoft.com/fwlink/?linkid=864501

        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }
    });
}

WindowsAuthMiddleware.cs

public class WindowsAuthMiddleware
{
    private readonly RequestDelegate next;

    public WindowsAuthMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (!context.User.Identity.IsAuthenticated)
        {
            await context.ChallengeAsync(IISDefaults.AuthenticationScheme);
            return;
        }

        await next(context);
    }
}

web.config

<system.webServer>
  <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true"/>
  <security>
    <authentication>
      <anonymousAuthentication enabled="true" />
      <windowsAuthentication enabled="true" />
    </authentication>
  </security>
</system.webServer>

2 个答案:

答案 0 :(得分:2)

所以,我花了几天的时间研究这个问题,并且找到了一个可行的解决方案(如果有点棘手)。

事实证明,主要问题是IIS将为应用程序发送的所有401响应处理Windows身份验证协商。在IIS中(或在system.webServer部分中启用Windows身份验证后,便会在较低级别上完成此操作,而我还没有找到一种绕过此行为的方法。我实际上是使用经典的Web窗体应用程序进行了测试,并且它的工作原理相同-我从未注意到的原因是,经典的窗体身份验证很少生成401响应,而是使用重定向(30x)将用户带到登录页面。 / p>

这给了我一个主意:我可以在管道中添加另一个中间件,以将授权基础结构生成的401响应重写为另一个很少使用的HTTP代码,并在客户端Angular应用中检测到该响应以使其表现为401(通过刷新访问令牌或拒绝路由器导航等)。我使用HTTP错误418“我是茶壶”,因为它是现有的但未使用的代码。这是代码:

ReplaceHttp401StatusCodeMiddleware.cs

public class ReplaceHttp401StatusCodeMiddleware
{
    private readonly RequestDelegate next;

    public ReplaceHttp401StatusCodeMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        await next(context);

        if (context.Response.StatusCode == 401)
        {
            // Replace all 401 responses, except the ones under the /sso paths
            // which will let IIS trigger the Windows Authentication mechanisms
            if (!context.Request.Path.StartsWithSegments("/sso"))
            {
                context.Response.StatusCode = 418;
                context.Response.Headers["X-Original-HTTP-Status-Code"] = "401";
            }
        }
    }
}

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...

    // Enable the SSO login using Windows Authentication
    app.UseWhen(
            context => context.Request.Path.StartsWithSegments("/sso"),
            builder => builder.UseMiddleware<WindowsAuthMiddleware>());
    app.UseMiddleware<ReplaceHttp401StatusCodeMiddleware>();

    ...
}

中间件还将原始状态代码注入响应中,以供进一步参考。

我还将MickaëlDerriey的建议应用于我的代码,以使用授权策略,因为它可以使控制器更整洁,但是解决方案不一定必须起作用。

答案 1 :(得分:0)

欢迎使用StackOverflow!这是一个有趣的问题。 首先,让我说一下,我没有测试此答案中的任何内容。

使用授权策略来驱动身份验证来源

我喜欢您创建的WindowsAuthMiddleware背后的想法,以及如果URL以/sso开头的情况下如何有条件地将其插入管道中。

MVC与授权系统集成在一起,并提供与授权策略相同的功能。结果是相同的,并且使您不必编写低级代码。

您可以在ConfigureServices方法中定义授权策略。就您而言,如果我没记错的话,有两项政策:

  • /sso的所有请求都应使用Windows身份验证进行身份验证;和
  • 所有其他请求都应通过JWT进行身份验证
services.AddAuthorization(options =>
{
    options.AddPolicy("Windows", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(IISDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .Build());

    options.AddPolicy("JWT", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .Build());
});

然后,您可以在用于装饰控制器和/或操作的[Authorize]属性中按名称引用这些策略。

[Authorize("Windows")]
public class SsoController : Controller
{
    // Actions
}

[Authorize("JWT")]
public class ApiController : Controller
{
    // Actions
}

这样做意味着Windows身份验证处理程序将不会针对/api请求运行,因此响应中不应包含WWW-Authenticate: NTLM和WWW-Authenticate: Negotiate标头。

删除所有请求的自动身份验证

当您将身份验证方案作为AddAuthentication的参数传递时,这意味着身份验证中间件将尝试根据该方案对每个请求进行身份验证。

当您拥有一个身份验证方案时,这很有用,但是在这种情况下,您可以考虑删除它,因为即使对于/sso的请求,JWT处理程序也会分析该请求以获取令牌。

两次致电AddAuthentication

您只能打一次AddAuthentication电话:

  • 第一个将IIS身份验证方案设置为默认身份,因此处理程序应在每个请求上运行;
  • 第二个调用将覆盖该设置,并将JWT方案设置为默认方案

让我知道你的生活!