如何验证通过 cookie 传递的 JWT? [英] How can I validate a JWT passed via cookies?

查看:17
本文介绍了如何验证通过 cookie 传递的 JWT?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

The UseJwtBearerAuthentication middleware in ASP.NET Core makes it easy to validate incoming JSON Web Tokens in Authorization headers.

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

How do I authenticate a JWT passed via cookies, instead of a header? Something like UseCookieAuthentication, but for a cookie that just contains a JWT.

推荐答案

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

I suggest you take a look at the following link.

https://stormpath.com/blog/token-authentication-asp-网核

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

They store JWT token in an http only cookie to prevent XSS attacks.

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

They then validate the JWT token in the cookie by adding the following code in the Startup.cs:

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

其中 CustomJwtDataFormat() 是此处定义的自定义格式:

Where CustomJwtDataFormat() is their custom format defined here:

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 令牌的代码,以了解这个想法:

Another solution would be to write some custom middleware that would intercept each request, look if it has a cookie, extract the JWT from the cookie and add an Authorization header on the fly before it reaches the Authorize filter of your controllers. Here is some code that work for OAuth tokens, to get the idea:

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 是以下类:

... where AccessToken is the following class:

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

希望这会有所帮助.

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

NOTE: It is also important to note that this way of doing things (token in http only cookie) will help prevent XSS attacks but however does not immune against Cross Site Request Forgery (CSRF) attacks, you must therefore also use anti-forgery tokens or set custom headers to prevent those.

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

Moreover, if you do not do any content sanitization, an attacker can still run an XSS script to make requests on behalf of the user, even with http only cookies and CRSF protection enabled. However, the attacker will not be able to steal the http only cookies that contain the tokens, nor will the attacker be able to make requests from a third party website.

因此,您仍然应该对用户生成的内容(例如评论等)进行大量清理...

You should therefore still perform heavy sanitization on user-generated content such as comments etc...

在评论中写道,链接的博客文章和代码是几天前在提出这个问题后由 OP 自己编写的.

It was written in the comments that the blog post linked and the code have been written by the OP himself a few days ago after asking this question.

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

For those who are interested in another "token in a cookie" approach to reduce XSS exposure they can use oAuth middleware such as the OpenId Connect Server in ASP.NET Core.

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

In the method of the token provider that is invoked to send the token back (ApplyTokenResponse()) to the client you can serialize the token and store it into a cookie that is http only:

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:

Then you need to make sure each request has the cookie attached to it. You must also write some middleware to intercept the cookie and set it to the 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()中:

In Configure() of startup.cs:

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

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

You can then use the Authorize attribute normally.

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

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

This solution works for both api and mvc apps. For ajax and fetch requests however your must write some custom middleware that will not redirect the user to the login page and instead return a 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;
        }
    }
}

这篇关于如何验证通过 cookie 传递的 JWT?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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