【问题标题】:How can I validate a JWT passed via cookies?如何验证通过 cookie 传递的 JWT?
【发布时间】:2016-09-20 18:20:15
【问题描述】:

ASP.NET Core 中的 UseJwtBearerAuthentication 中间件可以轻松验证 Authorization 标头中传入的 JSON Web 令牌。

如何验证通过 cookie 而不是标头传递的 JWT?类似于UseCookieAuthentication,但用于只包含 JWT 的 cookie。

【问题讨论】:

  • 好奇:如果你想使用 cookie 来流动它们,那么使用不记名令牌有什么意义?使用不记名令牌而不是 cookie 的全部目的是避免 XSRF 攻击等安全问题。如果您在等式中重新引入 cookie,您就重新引入了它的威胁模型。
  • @Pinpoint JWT 并不是严格意义上的不记名令牌;它们可以通过 Bearer 标头或 cookie 使用。我正在使用 JWT 进行无状态“会话”,但仍将它们存储在 cookie 中,因为浏览器支持很简单。 XSS 通过 cookie 标志缓解。
  • 1.根据定义,JWT 是不记名令牌或 PoP 令牌(在第一种情况下,您不需要证明您是令牌的合法持有者,在第二种情况下,您需要向服务器提供所有权证明)。 2. 使用 JWT 来表示一个“会话”并将它们存储在一个身份验证 cookie 中(它本身就是一个“会话”)是没有意义的,恐怕。 3. XSS 与 XSRF 无关,是完全不同的威胁。
  • @Pinpoint 我正在进行令牌身份验证并将访问令牌 JWT 存储在(纯文本)cookie 中,而不是 HTML5 存储中。我意识到 XSS != XSRF,你是绝对正确的。我应该澄清一下:我选择 cookie 是为了针对 XSS 提供强大的安全性,这确实意味着我会处理 CSRF 问题。
  • TBH,您的场景听起来确实像是令牌和 cookie 之间的奇怪组合。如果你真的要使用cookie,那么就完全不要使用token认证,直接使用cookie进行认证。您将不得不处理 XSRF 风险,但它与您尝试实现的场景没有什么不同。恕我直言,这并不值得,特别是当你知道这样做并不能真正减轻 XSS 攻击时:不要忘记,如果我不能窃取 HttpOnly cookie,没有什么能阻止我代表发送恶意 API 请求当您的 JS 应用程序中存在 XSS 缺陷时,用户。

标签: c# asp.net-core cookies jwt


【解决方案1】:

你也可以使用JwtBearerOptions类的Events.OnMessageReceived属性

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie()
.AddJwtBearer(options =>
{
    options.Events = new()
    {
        OnMessageReceived = context =>
        {
            var request = context.HttpContext.Request;
            var cookies = request.Cookies;
            if (cookies.TryGetValue("AccessTokenCookieName",
                out var accessTokenValue))
            {
                context.Token = accessTokenValue;
            }
            return Task.CompletedTask;
        };
    };
})

【讨论】:

    【解决方案2】:

    我成功实现了中间件(基于 Darxtar 的回答):

    // TokenController.cs
    
    public IActionResult Some()
    {
        ...
    
        var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
    
        Response.Cookies.Append(
            "x",
            tokenString,
            new CookieOptions()
            {
                Path = "/"
            }
        );
    
        return StatusCode(200, tokenString);
    }
    
    
    // JWTInHeaderMiddleware.cs
    
    public class JWTInHeaderMiddleware
    {
        private readonly RequestDelegate _next;
    
        public JWTInHeaderMiddleware(RequestDelegate next)
        {
            _next = next;
        }
    
        public async Task Invoke(HttpContext context)
        {
            var name = "x";
            var cookie = context.Request.Cookies[name];
    
            if (cookie != null)
                if (!context.Request.Headers.ContainsKey("Authorization"))
                    context.Request.Headers.Append("Authorization", "Bearer " + cookie);
    
            await _next.Invoke(context);
        }
    }
    
    // Startup.cs
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...
    
        app.UseMiddleware<JWTInHeaderMiddleware>();
    
        ...
    }
    

    【讨论】:

    • 您使用此解决方案实现的 cookie 是否确实是 HttpOnly cookie?似乎发送到浏览器的 cookie 只是一个包含 JWT 令牌的常规 cookie,适合我的情况。
    • @O.MeeKoh 将 CookieOptions 更改为 new CookieOptions { HttpOnly = true });
    • 我实际上让这一切都很好地工作。一旦我重新启动我的前端服务器,它就开始工作了。我也在使用 same-site = strict,这应该比 localStorage 更安全。
    【解决方案3】:

    我建议你看看下面的链接。

    https://stormpath.com/blog/token-authentication-asp-net-core

    他们将 JWT 令牌存储在仅限 http 的 cookie 中以防止 XSS 攻击。

    然后他们通过在 Startup.cs 中添加以下代码来验证 cookie 中的 JWT 令牌:

    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,
        AuthenticationScheme = "Cookie",
        CookieName = "access_token",
        TicketDataFormat = new CustomJwtDataFormat(
            SecurityAlgorithms.HmacSha256,
            tokenValidationParameters)
    });
    

    CustomJwtDataFormat() 是这里定义的自定义格式:

    public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket>
    {
        private readonly string algorithm;
        private readonly TokenValidationParameters validationParameters;
    
        public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
        {
            this.algorithm = algorithm;
            this.validationParameters = validationParameters;
        }
    
        public AuthenticationTicket Unprotect(string protectedText)
            => Unprotect(protectedText, null);
    
        public AuthenticationTicket Unprotect(string protectedText, string purpose)
        {
            var handler = new JwtSecurityTokenHandler();
            ClaimsPrincipal principal = null;
            SecurityToken validToken = null;
    
            try
            {
                principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);
    
                var validJwt = validToken as JwtSecurityToken;
    
                if (validJwt == null)
                {
                    throw new ArgumentException("Invalid JWT");
                }
    
                if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
                {
                    throw new ArgumentException($"Algorithm must be '{algorithm}'");
                }
    
                // Additional custom validation of JWT claims here (if any)
            }
            catch (SecurityTokenValidationException)
            {
                return null;
            }
            catch (ArgumentException)
            {
                return null;
            }
    
            // Validation passed. Return a valid AuthenticationTicket:
            return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie");
        }
    
        // This ISecureDataFormat implementation is decode-only
        public string Protect(AuthenticationTicket data)
        {
            throw new NotImplementedException();
        }
    
        public string Protect(AuthenticationTicket data, string purpose)
        {
            throw new NotImplementedException();
        }
    }
    

    另一种解决方案是编写一些自定义中间件来拦截每个请求,查看它是否有 cookie,从 cookie 中提取 JWT 并在到达控制器的 Authorize 过滤器之前动态添加 Authorization 标头。下面是一些适用于 OAuth 令牌的代码,以了解这个想法:

    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Logging;
    
    namespace MiddlewareSample
    {
        public class JWTInHeaderMiddleware
        {
            private readonly RequestDelegate _next;
    
            public JWTInHeaderMiddleware(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task Invoke(HttpContext context)
            {
               var authenticationCookieName = "access_token";
               var cookie = context.Request.Cookies[authenticationCookieName];
               if (cookie != null)
               {
                   var token = JsonConvert.DeserializeObject<AccessToken>(cookie);
                   context.Request.Headers.Append("Authorization", "Bearer " + token.access_token);
               }
    
               await _next.Invoke(context);
            }
        }
    }
    

    ...其中 AccessToken 是以下类:

    public class AccessToken
    {
        public string token_type { get; set; }
        public string access_token { get; set; }
        public string expires_in { get; set; }
    }
    

    希望这会有所帮助。

    注意:同样重要的是要注意,这种处理方式(仅在 http cookie 中的令牌)将有助于防止 XSS 攻击,但不能抵抗跨站请求伪造 (CSRF) 攻击,因此您还必须使用反-伪造令牌或设置自定义标头以防止这些。

    此外,如果您不进行任何内容清理,攻击者仍然可以运行 XSS 脚本来代表用户发出请求,即使启用了仅 http cookie 和 CRSF 保护。但是,攻击者将无法窃取包含令牌的仅 http cookie,攻击者也无法从第三方网站发出请求。

    因此,您仍应对用户生成的内容(例如 cmets 等)执行严格的清理...

    编辑:在博客文章链接的 cmets 中写了代码,几天前在问了这个问题后,OP 自己编写了代码。

    对于那些对另一种“cookie 中的令牌”方法感兴趣以减少 XSS 暴露的人,他们可以使用 oAuth 中间件,例如 ASP.NET Core 中的 OpenId Connect Server。

    在被调用以将令牌发回 (ApplyTokenResponse()) 到客户端的令牌提供程序的方法中,您可以序列化令牌并将其存储到仅限 http 的 cookie 中:

    using System.Security.Claims;
    using System.Threading.Tasks;
    using AspNet.Security.OpenIdConnect.Extensions;
    using AspNet.Security.OpenIdConnect.Server;
    using Newtonsoft.Json;
    
    namespace Shared.Providers
    {
    public class AuthenticationProvider : OpenIdConnectServerProvider
    {
    
        private readonly IApplicationService _applicationservice;
        private readonly IUserService _userService;
        public AuthenticationProvider(IUserService userService, 
                                      IApplicationService applicationservice)
        {
            _applicationservice = applicationservice;
            _userService = userService;
        }
    
        public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
        {
            if (string.IsNullOrEmpty(context.ClientId))
            {
                context.Reject(
                    error: OpenIdConnectConstants.Errors.InvalidRequest,
                    description: "Missing credentials: ensure that your credentials were correctly " +
                                 "flowed in the request body or in the authorization header");
    
                return Task.FromResult(0);
            }
    
            #region Validate Client
            var application = _applicationservice.GetByClientId(context.ClientId);
    
                if (applicationResult == null)
                {
                    context.Reject(
                                error: OpenIdConnectConstants.Errors.InvalidClient,
                                description: "Application not found in the database: ensure that your client_id is correct");
    
                    return Task.FromResult(0);
                }
                else
                {
                    var application = applicationResult.Data;
                    if (application.ApplicationType == (int)ApplicationTypes.JavaScript)
                    {
                        // Note: the context is marked as skipped instead of validated because the client
                        // is not trusted (JavaScript applications cannot keep their credentials secret).
                        context.Skip();
                    }
                    else
                    {
                        context.Reject(
                                error: OpenIdConnectConstants.Errors.InvalidClient,
                                description: "Authorization server only handles Javascript application.");
    
                        return Task.FromResult(0);
                    }
                }
            #endregion Validate Client
    
            return Task.FromResult(0);
        }
    
        public override async Task HandleTokenRequest(HandleTokenRequestContext context)
        {
            if (context.Request.IsPasswordGrantType())
            {
                var username = context.Request.Username.ToLowerInvariant();
                var user = await _userService.GetUserLoginDtoAsync(
                    // filter
                    u => u.UserName == username
                );
    
                if (user == null)
                {
                    context.Reject(
                            error: OpenIdConnectConstants.Errors.InvalidGrant,
                            description: "Invalid username or password.");
                    return;
                }
                var password = context.Request.Password;
    
                var passWordCheckResult = await _userService.CheckUserPasswordAsync(user, context.Request.Password);
    
    
                if (!passWordCheckResult)
                {
                    context.Reject(
                            error: OpenIdConnectConstants.Errors.InvalidGrant,
                            description: "Invalid username or password.");
                    return;
                }
    
                var roles = await _userService.GetUserRolesAsync(user);
    
                if (!roles.Any())
                {
                    context.Reject(
                            error: OpenIdConnectConstants.Errors.InvalidRequest,
                            description: "Invalid user configuration.");
                    return;
                }
            // add the claims
            var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
            identity.AddClaim(ClaimTypes.NameIdentifier, user.Id, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
            identity.AddClaim(ClaimTypes.Name, user.UserName, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
             // add the user's roles as claims
            foreach (var role in roles)
            {
                identity.AddClaim(ClaimTypes.Role, role, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
            }
             context.Validate(new ClaimsPrincipal(identity));
            }
            else
            {
                context.Reject(
                        error: OpenIdConnectConstants.Errors.InvalidGrant,
                        description: "Invalid grant type.");
                return;
            }
    
            return;
        }
    
        public override Task ApplyTokenResponse(ApplyTokenResponseContext context)
        {
            var token = context.Response.Root;
    
            var stringified = JsonConvert.SerializeObject(token);
            // the token will be stored in a cookie on the client
            context.HttpContext.Response.Cookies.Append(
                "exampleToken",
                stringified,
                new Microsoft.AspNetCore.Http.CookieOptions()
                {
                    Path = "/",
                    HttpOnly = true, // to prevent XSS
                    Secure = false, // set to true in production
                    Expires = // your token life time
                }
            );
    
            return base.ApplyTokenResponse(context);
        }
    }
    }
    

    然后您需要确保每个请求都附加了 cookie。您还必须编写一些中间件来拦截cookie并将其设置为header:

    public class AuthorizationHeader
    {
        private readonly RequestDelegate _next;
    
        public AuthorizationHeader(RequestDelegate next)
        {
            _next = next;
        }
    
        public async Task Invoke(HttpContext context)
        {
            var authenticationCookieName = "exampleToken";
            var cookie = context.Request.Cookies[authenticationCookieName];
            if (cookie != null)
            {
    
                if (!context.Request.Path.ToString().ToLower().Contains("/account/logout"))
                {
                    if (!string.IsNullOrEmpty(cookie))
                    {
                        var token = JsonConvert.DeserializeObject<AccessToken>(cookie);
                        if (token != null)
                        {
                            var headerValue = "Bearer " + token.access_token;
                            if (context.Request.Headers.ContainsKey("Authorization"))
                            {
                                context.Request.Headers["Authorization"] = headerValue;
                            }else
                            {
                                context.Request.Headers.Append("Authorization", headerValue);
                            }
                        }
                    }
                    await _next.Invoke(context);
                }
                else
                {
                    // this is a logout request, clear the cookie by making it expire now
                    context.Response.Cookies.Append(authenticationCookieName,
                                                    "",
                                                    new Microsoft.AspNetCore.Http.CookieOptions()
                                                    {
                                                        Path = "/",
                                                        HttpOnly = true,
                                                        Secure = false,
                                                        Expires = DateTime.UtcNow.AddHours(-1)
                                                    });
                    context.Response.Redirect("/");
                    return;
                }
            }
            else
            {
                await _next.Invoke(context);
            }
        }
    }
    

    在startup.cs的Configure()中:

        // use the AuthorizationHeader middleware
        app.UseMiddleware<AuthorizationHeader>();
        // Add a new middleware validating access tokens.
        app.UseOAuthValidation();
    

    然后就可以正常使用Authorize属性了。

        [Authorize(Roles = "Administrator,User")]
    

    此解决方案适用于 api 和 mvc 应用程序。但是对于 ajax 和 fetch 请求,您必须编写一些不会将用户重定向到登录页面而是返回 401 的自定义中间件:

    public class RedirectHandler
    {
        private readonly RequestDelegate _next;
    
        public RedirectHandler(RequestDelegate next)
        {
            _next = next;
        }
    
        public bool IsAjaxRequest(HttpContext context)
        {
            return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
        }
    
        public bool IsFetchRequest(HttpContext context)
        {
            return context.Request.Headers["X-Requested-With"] == "Fetch";
        }
    
        public async Task Invoke(HttpContext context)
        {
            await _next.Invoke(context);
            var ajax = IsAjaxRequest(context);
            var fetch = IsFetchRequest(context);
            if (context.Response.StatusCode == 302 && (ajax || fetch))
            {
                context.Response.Clear();
                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                await context.Response.WriteAsync("Unauthorized");
                return;
            }
        }
    }
    

    【讨论】:

    • 我不得不问,你有没有检查过那篇博文的作者是谁? i.ytimg.com/vi/OGAu_DeKckI/hqdefault.jpg
    • 你的观点非常有道理,不,我没有检查作者。我会研究一个更客观的解决方案。我已经使用 oauth2 制作了一些等效的自定义身份验证验证,我将很快对其进行编辑以提供替代方案。
    • 大声笑,我仍然不确定您是否注意到:您将 OP 链接到他自己的博客文章和代码。这就是我要问的。
    • 是的,我注意到了。因此,我为什么要提供替代解决方案,这不仅仅是 OP 所写的。
    • 谢谢 Darxtar,在我的应用程序中实现了中间件解决方案,效果很好。
    猜你喜欢
    • 2019-06-05
    • 2019-04-23
    • 2019-02-08
    • 1970-01-01
    • 2020-10-09
    • 2020-09-03
    • 2020-01-06
    • 2018-03-03
    • 2022-06-25
    相关资源
    最近更新 更多