ASP.NET Core 2.0 将 Cookie 和 Bearer Authorization 结合到同一端点

ASP.NET Core 2.0 combining Cookies and Bearer Authorization for the same endpoint

我使用 "Web Application (Model-View-Controller)" 模板和“.Net Framework”+ "ASP.NET Core 2" 作为配置在 VS17 中创建了一个新的 ASP.NET 核心 Web 应用程序项目。身份验证配置设置为 "Individual User Accounts".

我有以下示例端点:

[Produces("application/json")]
[Route("api/price")]
[Authorize(Roles = "PriceViwer", AuthenticationSchemes = "Cookies,Bearer")]
public class PriceController : Controller
{

    public IActionResult Get()
    {
        return Ok(new Dictionary<string, string> { {"Galleon/Pound",
                                                   "999.999" } );
    }
}

"Cookies,Bearer" 是通过连接 CookieAuthenticationDefaults.AuthenticationSchemeJwtBearerDefaults.AuthenticationScheme 得到的。

objective 是为了能够配置端点的授权,以便可以使用令牌和 cookie 身份验证方法访问它。

这是我在 Startup.cs 中的身份验证设置:

    services.AddAuthentication()
        .AddCookie(cfg => { cfg.SlidingExpiration = true;})
        .AddJwtBearer(cfg => {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
            cfg.TokenValidationParameters = new TokenValidationParameters() {
                                                    ValidIssuer = Configuration["Tokens:Issuer"],
                                                    ValidAudience = Configuration["Tokens:Issuer"],
                                                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
                                                };
        });

因此,当我尝试使用浏览器访问端点时,我收到带有空白 html 页面的 401 响应。

然后我登录,当我再次尝试访问端点时,我得到了相同的响应。

然后,我尝试通过指定不记名令牌来访问端点。 returns 200 响应的预期结果。

那么,如果我删除 [Authorize(AuthenticationSchemes = "Cookies,Bearer")],情况就相反了 - cookie 身份验证有效并且 returns 200,但是与上面使用的相同的不记名令牌方法没有给出任何结果并且只需重定向到默认的 AspIdentity 登录页面。

我在这里看到两个可能的问题:

1) ASP.NET 核心不允许 'combined' 身份验证。 2) 'Cookies' 不是有效的架构名称。但是,什么是正确的使用呢?

请指教。谢谢。

我认为您不需要将 AuthenticationScheme 设置到您的控制器。只需像这样在 ConfigureServices 中使用经过身份验证的用户:

// requires: using Microsoft.AspNetCore.Authorization;
//           using Microsoft.AspNetCore.Mvc.Authorization;
services.AddMvc(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

关于我的来源的文档:registerAuthorizationHandlers

对于方案密钥是否无效这一部分,您可以使用内插字符串来使用正确的密钥:

[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}")]

编辑: 我做了进一步的研究并得出以下结论: 不可能用两个 Schemes Or-Like 来授权一个方法,但是你可以使用两个 public 方法来调用这样的私有方法:

//private method
private IActionResult GetThingPrivate()
{
   //your Code here
}

//Jwt-Method
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("bearer")]
public IActionResult GetByBearer()
{
   return GetThingsPrivate();
}

 //Cookie-Method
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[HttpGet("cookie")]
public IActionResult GetByCookie()
{
   return GetThingsPrivate();
}

如果我正确理解问题,那么我相信有解决方案。在以下示例中,我在单个应用程序中使用 cookie 和不记名身份验证。 [Authorize] 属性可以在不指定方案的情况下使用,应用程序将根据使用的授权方法动态做出反应。

两次调用

services.AddAuthentication 以注册 2 个身份验证方案。 解决方案的关键是在代码片段末尾调用 services.AddAuthorization,它告诉 ASP.NET 使用两种方案。

我已经测试过了,它似乎运行良好。

(基于 Microsoft docs。)

services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "https://localhost:4991";
        options.RequireHttpsMetadata = false;

        options.ClientId = "WebApp";
        options.ClientSecret = "secret";

        options.ResponseType = "code id_token";
        options.Scope.Add("api");
        options.SaveTokens = true;
    });

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:4991";
        options.RequireHttpsMetadata = false;
        // name of the API resource
        options.Audience = "api";
    });

services.AddAuthorization(options =>
{
    var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
        CookieAuthenticationDefaults.AuthenticationScheme,
        JwtBearerDefaults.AuthenticationScheme);
    defaultAuthorizationPolicyBuilder =
        defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
    options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});

编辑

这适用于经过身份验证的用户,但如果用户尚未登录,则只是 returns 401(未授权)。

为确保未经授权的用户被重定向到登录页面,请将以下代码添加到 Startup class 中的 Configure 方法中。注意:必须将新中间件放在 之后 调用 app.UseAuthentication().

app.UseAuthentication();
app.Use(async (context, next) =>
{
    await next();
    var bearerAuth = context.Request.Headers["Authorization"]
        .FirstOrDefault()?.StartsWith("Bearer ") ?? false;
    if (context.Response.StatusCode == 401
        && !context.User.Identity.IsAuthenticated
        && !bearerAuth)
    {
        await context.ChallengeAsync("oidc");
    }
});

如果您知道实现此重定向的更简洁的方法,请post发表评论!

使用 Asp.net 核心 2.2

测试
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
    {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:4991";
        options.RequireHttpsMetadata = false;
        // name of the API resource
        options.Audience = "api";
    });


services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "https://localhost:4991";
        options.RequireHttpsMetadata = false;

        options.ClientId = "WebApp";
        options.ClientSecret = "secret";

        options.ResponseType = "code id_token";
        options.Scope.Add("api");
        options.SaveTokens = true;
    });

services.AddAuthorization(options =>
{   
    // Add policies for API scope claims
     options.AddPolicy(AuthorizationConsts.ReadPolicy,
        policy => policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                ((c.Type == AuthorizationConsts.ScopeClaimType && c.Value == AuthorizationConsts.ReadScope)
                || (c.Type == AuthorizationConsts.IdentityProviderClaimType))) && context.User.Identity.IsAuthenticated
        ));
    // No need to add default policy here
});


app.UseAuthentication();
app.UseCookiePolicy();

在控制器中,添加必要的授权属性

[Authorize(AuthenticationSchemes = AuthorizationConsts.BearerOrCookiesAuthenticationScheme, Policy = AuthorizationConsts.ReadPolicy)]

帮手来了class

public class AuthorizationConsts
{
    public const string BearerOrCookiesAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme + "," + IdentityServerAuthenticationDefaults.AuthenticationScheme;
    public const string IdentityProviderClaimType = "idp";
    public const string ScopeClaimType = "scope";
    public const string ReadPolicy = "RequireReadPolicy";
    public const string ReadScope = "data:read";
}

经过数小时的研究和摸不着头脑,这就是我在 ASP.NET Core 2.2 -> ASP.NET 5.0:

中工作的方法
  • 使用 .AddCookie() 和 .AddJwtBearer() 配置方案
  • 使用自定义策略方案转发到正确的身份验证方案。

您不需要在每个控制器操作上指定方案,并且对两者都适用。 [授权]就够了

services.AddAuthentication( config =>
{
    config.DefaultScheme = "smart";
} )
.AddPolicyScheme( "smart", "Bearer or Jwt", options =>
{
    options.ForwardDefaultSelector = context =>
    {
        var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith( "Bearer " ) ?? false;
        // You could also check for the actual path here if that's your requirement:
        // eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
        if ( bearerAuth )
            return JwtBearerDefaults.AuthenticationScheme;
        else
            return CookieAuthenticationDefaults.AuthenticationScheme;
    };
} )
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
    options.LoginPath = new PathString( "/Account/Login" );
    options.AccessDeniedPath = new PathString( "/Account/Login" );
    options.LogoutPath = new PathString( "/Account/Logout" );
    options.Cookie.Name = "CustomerPortal.Identity";
    options.SlidingExpiration = true;
    options.ExpireTimeSpan = TimeSpan.FromDays( 1 ); //Account.Login overrides this default value
} )
.AddJwtBearer( JwtBearerDefaults.AuthenticationScheme, options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey( key ),
        ValidateIssuer = false,
        ValidateAudience = false
    };
} );

services.AddAuthorization( options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder( CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme )
        .RequireAuthenticatedUser()
        .Build();
} );

我有一个场景,我需要单独使用 Bearer 或 Cookie 来下载文件 api。所以以下解决方案对我有用。

如下所示配置服务。

services.AddAuthentication(options =>
{
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(options =>
{
    options.Authority = gatewayUrl;
})
.AddOpenIdConnect(options =>
{
    // Setting default signin scheme for openidconnect makes it to force 
    // use cookies handler for signin 
    // because jwthandler doesnt have SigninAsync implemented
    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.Authority = "https://youridp.com";
    options.ClientId = "yourclientid";
    options.CallbackPath = "/signin-oidc";
    options.ResponseType = OpenIdConnectResponseType.Code;
});

然后如下所示配置您的控制器。

[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer,OpenIdConnect")]
public async Task<IActionResult> Download([FromQuery(Name = "token")] string token)
{
    ///your code goes here.
    ///My file download api will work with both bearer or automatically authenticate with cookies using OpenidConnect.
}

Christo Carstens,答案对我来说非常有效。 我只是想分享我添加到他的 AddPolicyScheme 中的额外检查。 (往上看) 在我的例子中,问题是我有一个 Azure Web 服务,它使用 JWT 处理我所有的移动应用程序请求,但我还需要它作为使用 cookie 的 Google/Apple/Facebook 身份验证的网关。 我按照建议更新了我的启动

.AddPolicyScheme( "smart", "Bearer or Jwt", options =>
{
    options.ForwardDefaultSelector = context =>
    {
        var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith( "Bearer " ) ?? false;
        // You could also check for the actual path here if that's your requirement:
        // eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
        if ( bearerAuth )
            return JwtBearerDefaults.AuthenticationScheme;
        else
            return CookieAuthenticationDefaults.AuthenticationScheme;
    };
} )

我唯一的问题是,如果对我的任何 api 调用设置了 [Authorize] 属性,并且 header 中没有“授权”键,然后它将使用 Cookie 授权和 return 未找到 (404) 而不是未授权 (401)。 他检查路径的建议奏效了,但我想对将来可能没有该路径的任何方法强制执行 JWT。 最后我选择了这段代码。

.AddPolicyScheme("CookieOrJWT", "Bearer or Jwt", options =>
                {
                    options.ForwardDefaultSelector = context =>
                    {
                        var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
                        
                        if (bearerAuth)
                            return JwtBearerDefaults.AuthenticationScheme;
                        else
                        {
                            var ep = context.GetEndpoint();
                            var requiresAuth = ep?.Metadata?.GetMetadata<AuthorizeAttribute>();
                            return requiresAuth != null 
                                ? JwtBearerDefaults.AuthenticationScheme
                            : CookieAuthenticationDefaults.AuthenticationScheme;
                        }
                    };
                })

通过检查端点元数据(仅在 Authorization 不在 header 中的极少数情况下),我可以为使用 [Authorize] 属性修饰的任何方法设置 JwtBearerDefaults.AuthenticationScheme。 即使该方法从它的 class 继承 [Authorize] 属性并且没有明确设置它,这仍然有效。 例如

[ApiController]
[Route("api/[Controller]")]
[Authorize]
public class MyController : ControllerBase {
  
    [HttpGet]
    public ActionResult MyWebRequestThatRequiresAuthorization() {
       return true;
    }
}

感谢 Christo Carstens 提供的解决方案。我为此伤脑筋。为我节省了无数小时。