ASP.NET Core WebAPI Cookie + JWT身份验证

36
我们有一个SPA(Angular)和API后端(ASP.NET Core WebAPI):
SPA监听的是app.mydomain.com,API监听的是app.mydomain.com/API。
我们使用JWT进行身份验证,内置Microsoft.AspNetCore.Authentication.JwtBearer;我有一个控制器app.mydomain.com/API/auth/jwt/login来创建令牌。SPA将它们保存在本地存储中。一切工作正常。但在进行安全审计后,我们被告知要将本地存储更改为cookie。
问题是,监听app.mydomain.com/API的API除了被SPA使用,还会被移动应用程序和几个客户机服务器解决方案使用。
因此,我们必须保持JWT不变,但添加Cookies。我找到了几篇文章,这些文章在不同的控制器上结合使用Cookies和JWT,但我需要它们在每个控制器上并行工作。
如果客户端发送cookie,则通过cookie进行身份验证。如果客户端发送JWT承载者,则通过JWT进行身份验证。
这是否可以通过内置的ASP.NET身份验证或DIY中间件实现?
谢谢!

1
可能仍然比cookie和webapi更好。如果攻击者能够引诱用户到任何其他网站或隐藏表单,他可以使用已登录用户的权限执行操作,而您几乎无法控制。此外,Antiforgery请求需要一个状态(cookie和正确的令牌在服务器上进行比较),这违反了REST服务的“无状态”特性。在SPA中发出新的AntiRequest伪造令牌并不直观,您需要在发送请求之前每次请求服务器以获取下一个请求有效的新令牌。 - Tseng
2
在我看来,您最好使用不透明的令牌(或IdentityServer 4术语中的参考令牌)。仍然需要在每个请求中发送令牌,但是您可以启用令牌验证,这样您就可以相对快速地撤销令牌以防止其遭到损害并被使用。此外,您可以在颁发令牌时将用户IP放入令牌中,如果IP更改,则令牌会失效。对用户来说可能有些麻烦,但它可以防止攻击者自己使用访问令牌或刷新令牌(除非用户再次能够将JavaScript代码注入应用程序中)。 - Tseng
3
无论是Cookie还是JWT的方法都容易受到代码注入攻击。Http Cookie不允许攻击者窃取cookie,但他仍然可以代表已登录用户执行操作。对于存储在本地存储中的JWT Cookie,除了可以窃取令牌本身之外,情况也是如此。但是,可以通过将IP作为声明放入令牌并在服务器上验证它或至少使其更加困难来防止这种情况(IP可以被欺骗,但攻击者无法获得任何响应)。这是一个复杂的话题。 - Tseng
1
感谢您的努力。我们将重新开放安全审计建议,并进行头脑风暴@工作。 - Luke1988
请看这里的答案 https://dev59.com/gVYN5IYBdhLWcg3wxqhu - Amin AmiriDarban
显示剩余6条评论
8个回答

14

好的,我尝试实现这个功能已经有一段时间了,并且使用了以下代码解决了使用JWT身份验证令牌和Cookie身份验证的相同问题。

API服务提供商UserController.cs

此提供程序为用户提供不同的服务,同时具有(Cookie和JWT Bearer)身份验证方案。

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] 
[Route("[controller]")]
[ApiController]
public class UsersController : ControllerBase
{ 
    private readonly IUserServices_Api _services;
    public UsersController(IUserServices_Api services)
    {
        this._services = services;
    }
     
    [HttpGet]
    public IEnumerable<User> Getall()
    {
        return _services.GetAll();
    }
}

我的Startup.cs

public void ConfigureServices(IServiceCollection services)
    {
          
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
         
        services.AddAuthentication(options => {
            options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddCookie(options =>
            {
                options.LoginPath = "/Account/Login";
                options.AccessDeniedPath = "/Home/Error";
            })
            .AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = " you site link blah blah",
                    ValidIssuer = "You Site link Blah  blah",
                    IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(sysController.GetSecurityKey()))
                    ,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                };
            });

    }

如果您想为特定控制器自定义身份验证,则需要指定授权的身份验证类型,例如:

[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public IActionResult Index()
{
    return View();    // This can only be Access when Cookie Authentication is Authorized.
}

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
    return View();    // And this one will be Access when JWT Bearer is Valid
}

你是否注意到在这样实施后,你有重复的声明?我也为 WebAPI 和 MVC 设置了 Jwt & Cookie,并且我有两组相同的声明。这有点让人烦恼。我已经在 Stack Overflow 上提出了一个问题 https://stackoverflow.com/questions/68558515/asp-net-core-identity-avoiding-duplicate-claims-when-using-cookies-and-jwt-authe ,但没有得到积极的回复。 - user2058413
是的,但那只是一个示例,我们必须更改方法名称,在这种情况下为Index() - Sayed Muhammad Idrees

7

我一直有同样的问题,刚在stackoverflow的另一个问题中找到了看起来是解决方法。

请看这个链接。

我会尝试使用该解决方案,并更新此答案的结果。

编辑:似乎不可能在同一方法中实现双重身份验证类型,但我提到的链接中提供的解决方案说:

It's not possible to authorize a method with two Schemes Or-Like, but you can use two public methods, to call a private method

//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();
}    
无论如何,您应该查看链接,它肯定会对我有所帮助。感谢Nikolaus提供的答案。

5

我没有找到很好的方法来解决这个问题 - 为了支持两种授权方案,不得不复制API很痛苦。

我一直在研究使用反向代理的想法,它看起来对此是一个很好的解决方案。

  1. 用户登录网站(使用cookie httpOnly进行会话)
  2. 网站使用防伪令牌
  3. SPA向网站服务器发送请求,并在标头中包括防伪令牌:https://app.mydomain.com/api/secureResource
  4. 网站服务器验证防伪令牌(CSRF)
  5. 网站服务器确定请求是针对API的,并应将其发送到反向代理
  6. 网站服务器获取用户访问API的访问令牌
  7. 反向代理将请求转发到API:https://api.mydomain.com/api/secureResource

请注意,防伪令牌(#2,#4)非常重要,否则您可能会暴露API以进行CSRF攻击。


示例(.NET Core 2.1 MVC with IdentityServer4):

为了获得此的工作示例,我从IdentityServer4快速入门切换到混合流并添加API访问开始。 这设置了我所追求的情况,在此情况下,MVC应用程序使用cookie并可以向身份服务器请求access_token以调用API。

我使用Microsoft.AspNetCore.Proxy作为反向代理并修改了快速入门。

MVC Startup.ConfigureServices:

services.AddAntiforgery();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

MVC启动.Configure:

app.MapWhen(IsApiRequest, builder =>
{
    builder.UseAntiforgeryTokens();

    var messageHandler = new BearerTokenRequestHandler(builder.ApplicationServices);
    var proxyOptions = new ProxyOptions
    {
        Scheme = "https",
        Host = "api.mydomain.com",
        Port = "443",
        BackChannelMessageHandler = messageHandler
    };
    builder.RunProxy(proxyOptions);
});

private static bool IsApiRequest(HttpContext httpContext)
{
    return httpContext.Request.Path.Value.StartsWith(@"/api/", StringComparison.OrdinalIgnoreCase);
}

ValidateAntiForgeryToken (Marius Schulz):

public class ValidateAntiForgeryTokenMiddleware
{
    private readonly RequestDelegate next;
    private readonly IAntiforgery antiforgery;

    public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
    {
        this.next = next;
        this.antiforgery = antiforgery;
    }

    public async Task Invoke(HttpContext context)
    {
        await antiforgery.ValidateRequestAsync(context);
        await next(context);
    }
}

public static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder UseAntiforgeryTokens(this IApplicationBuilder app)
    {
        return app.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
    }
}

BearerTokenRequestHandler:

public class BearerTokenRequestHandler : DelegatingHandler
{
    private readonly IServiceProvider serviceProvider;

    public BearerTokenRequestHandler(IServiceProvider serviceProvider, HttpMessageHandler innerHandler = null)
    {
        this.serviceProvider = serviceProvider;
        InnerHandler = innerHandler ?? new HttpClientHandler();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
        var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
        request.Headers.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
        var result = await base.SendAsync(request, cancellationToken);
        return result;
    }
}

_Layout.cshtml:

@Html.AntiForgeryToken()

那么,使用您的SPA框架,您可以发出请求。为了验证,我只是进行了一个简单的AJAX请求:
<a onclick="sendSecureAjaxRequest()">Do Secure AJAX Request</a>
<div id="ajax-content"></div>

<script language="javascript">
function sendSecureAjaxRequest(path) {
    var myRequest = new XMLHttpRequest();
    myRequest.open('GET', '/api/secureResource');
    myRequest.setRequestHeader("RequestVerificationToken",
        document.getElementsByName('__RequestVerificationToken')[0].value);
    myRequest.onreadystatechange = function () {
        if (myRequest.readyState === XMLHttpRequest.DONE) {
            if (myRequest.status === 200) {
                document.getElementById('ajax-content').innerHTML = myRequest.responseText;
            } else {
                alert('There was an error processing the AJAX request: ' + myRequest.status);
            }
        }  
    };
    myRequest.send();
};
</script>

这只是一个概念验证测试,你的结果可能有所不同,而且我对 .NET Core 和中间件配置还很陌生,代码看起来可能不够优美。我仅对此进行了有限的测试,并且只使用 GET 请求访问了 API,没有使用 SSL(https)。
如预期的那样,如果 AJAX 请求中的防伪标记被删除,请求将失败。如果用户未登录(经过身份验证),请求也会失败。
像往常一样,每个项目都是独特的,因此始终要验证是否满足安全要求。请查看此答案上留下的任何评论以了解可能引发的任何潜在安全问题。
另外,我认为一旦所有常用浏览器(即老版本浏览器被淘汰)都支持子资源完整性 (SRI) 和内容安全策略 (CSP),就应该重新评估使用本地存储来存储 API 令牌的情况,这将减少令牌存储的复杂性。现在应该使用 SRI 和 CSP 来帮助减少支持浏览器的攻击面。

4

使用此代码,您可以同时使用cookie和header。 如果cookie为空,则自动检查header。

将此代码添加到AddJwtBearer选项中。

options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        context.Token = context.Request.Cookies["Authorization"];
        return Task.CompletedTask;
    }
};

完整用法如下:

        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
            {
                options.RequireHttpsMetadata = false;
                options.SaveToken = false;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = Configuration["JwtToken:Audience"],
                    ValidIssuer = Configuration["JwtToken:Issuer"],
                    IssuerSigningKey =
                        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtToken:Key"]))
                };
                options.Events = new JwtBearerEvents
                {
                    OnMessageReceived = context =>
                    {
                        context.Token = context.Request.Cookies["Authorization"];
                        return Task.CompletedTask;
                    }
                };
            });

Header => Authorization: Bearer Your-Token

或者

Cookie => Authorization=Your-Token //不要在Cookie中添加Bearer


1
这对我来说是解决问题最干净的方法。您可以在“请求”对象上添加额外的过滤器,以过滤何时使用此回退。您可以检查“身份验证”标头是否存在,以使令牌优先级更高,或者检查路径以启用特定端点的此功能。 - RedMatt

4

我认为最简单的解决方案是由David Kirkland提出的:

创建组合授权策略(在ConfigureServices(IServiceCollection services))中:

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

Configure(IApplicationBuilder app)中添加将在401情况下重定向到登录页面的中间件:
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");
    }
});

1
如何修改中间件以支持XMLHttpRequest()? 在使用XMLHttpRequest进行AJAX调用时,当触发await context.ChallengeAsync()代码行时,它会返回状态码200和登录HTML页面。 - Bharat Vasant

3

在寻找将Firebase授权与Net Core Web API结合使用的过程中(网站使用cookie,移动应用程序使用授权标头),我们采用了以下解决方案。

public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
               .AddJwtBearer(options =>
               {
                   options.Authority = "https://securetoken.google.com/xxxxx";
                   options.TokenValidationParameters = new TokenValidationParameters
                   {
                       ValidateIssuer = true,
                       ValidIssuer = options.Authority,
                       ValidateAudience = true,
                       ValidAudience = "xxxxx",
                       ValidateLifetime = true
                   };
                   options.Events = new JwtBearerEvents
                   {
                       OnMessageReceived = context =>
                       {
                           if (context.Request.Cookies.ContainsKey(GlobalConst.JwtBearer))
                           {
                               context.Token = context.Request.Cookies[GlobalConst.JwtBearer];
                           }
                           else if (context.Request.Headers.ContainsKey("Authorization"))
                                {
                                    var authhdr = context.Request.Headers["Authorization"].FirstOrDefault(k=>k.StartsWith(GlobalConst.JwtBearer));
                                    if (!string.IsNullOrEmpty(authhdr))
                                    {
                                        var keyval = authhdr.Split(" ");
                                        if (keyval != null && keyval.Length > 1) context.Token = keyval[1];
                                    }
                                }
                           return Task.CompletedTask;
                       }
                   };
               });

这里

 public static readonly string JwtBearer = "Bearer";

看起来工作正常。从移动设备和Postman(用于cookie)进行了检查。


1
我找到了Rick Strahl写的这篇优秀文章。这是我迄今为止找到的最佳解决方案,而且我在.NET 5中使用了它。
下面是结合JWT令牌和Cookie身份验证在.NET应用程序中的关键代码:
services.AddAuthentication(options => 
{
    options.DefaultScheme = "JWT_OR_COOKIE";
    options.DefaultChallengeScheme = "JWT_OR_COOKIE";
})
.AddCookie( options =>
{
    options.LoginPath = "/login";
    options.ExpireTimeSpan = TimeSpan.FromDays(1);
})
.AddJwtBearer( options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = config.JwtToken.Issuer,
        ValidateAudience = true,
        ValidAudience = config.JwtToken.Audience,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
    };
})
.AddPolicyScheme("JWT_OR_COOKIE", "JWT_OR_COOKIE", options =>
{
    options.ForwardDefaultSelector = context =>
    {
        string authorization = context.Request.Headers[HeaderNames.Authorization];
        if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
            return JwtBearerDefaults.AuthenticationScheme;
            
        return CookieAuthenticationDefaults.AuthenticationScheme;
    };
});

-1

2
虽然这个链接可能回答了问题,但最好在此处包含答案的重要部分并提供链接以供参考。仅有链接的回答如果链接页面更改可能会变得无效。 - Uyghur Lives Matter
这将仅允许使用JWT调用方法,而不是使用cookies。 - Alexander Goldabin
@user369450 没问题。由于我无法在此处粘贴所有内容...我们单独发布了它https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login - Rahul Uttarkar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接