web-dev-qa-db-ja.com

Cookieを介して渡されたJWTを検証するにはどうすればよいですか?

ASP.NET CoreのUseJwtBearerAuthenticationミドルウェアを使用すると、Authorizationヘッダー内の着信JSON Webトークンを簡単に検証できます。

ヘッダーではなく、Cookieを介して渡されたJWTを認証するにはどうすればよいですか? UseCookieAuthenticationに似ていますが、JWTのみを含むCookie用です。

19
Nate Barbettini

次のリンクをご覧になることをお勧めします。

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

XSS攻撃を防ぐために、JWTトークンをhttpのみのCookieに保存します。

次に、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)攻撃には耐性がないことに注意することも重要です。したがって、偽造防止トークンも使用する必要があります。または、それらを防ぐためにカスタムヘッダーを設定します。

さらに、コンテンツのサニタイズを行わない場合、HTTPのみのCookieとCRSF保護が有効になっている場合でも、攻撃者はXSSスクリプトを実行してユーザーに代わってリクエストを行うことができます。ただし、攻撃者はトークンを含むhttpのみのCookieを盗むことはできず、第三者のWebサイトから要求を行うこともできません。

したがって、コメントなどのユーザー生成コンテンツに対しては、引き続き強力なサニタイズを実行する必要があります...

編集:ブログの投稿がリンクされており、コードはこの質問をした数日前にOP自身が書いたというコメントに書かれていました。

XSSの露出を減らすための別の「Cookieのトークン」アプローチに興味がある人は、ASP.NET CoreのOpenId Connect ServerなどのoAuthミドルウェアを使用できます。

トークンをクライアントに送信するために呼び出されるトークンプロバイダーのメソッド(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をインターセプトしてヘッダーに設定するミドルウェアを作成する必要があります。

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およびフェッチリクエストの場合、ユーザーをログインページにリダイレクトせず、代わりに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;
        }
    }
}
21
Darxtar

ミドルウェアを正常に実装しました(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>();

    ...
}
7
lolol