2016-05-23 6 views
7

Вредоносная программа UseJwtBearerAuthentication в ASP.NET Core упрощает проверку входящих веб-токенов JSON в заголовках Authorization.Как я могу проверить JWT, переданный через файлы cookie?

Как аутентифицировать JWT, переданный через файлы cookie, вместо заголовка? Что-то вроде UseCookieAuthentication, но для файла cookie, который просто содержит JWT.

+2

Любопытно: в чем смысл использования токенов-носителей, если вы хотите использовать файлы cookie для их переноса? Весь смысл использования токенов-носителей вместо куки-файлов - избегать проблем безопасности, таких как атаки XSRF. Если вы повторно вводите файлы cookie в уравнение, вы снова вводите свою модель угрозы. – Pinpoint

+1

@Pinpoint JWTs - это не только маркеры-носители; они могут использоваться либо через заголовок несущей, либо через файлы cookie. Я использую JWT для сеансов без состояния, но сохраняю их в файлах cookie, потому что поддержка браузера проста. XSS смягчается флагами cookie. –

+0

1. По определению, JWT являются либо знаками на предъявителя, либо PoP (в первом случае вам не нужно доказывать, что вы являетесь законным обладателем токена, во втором - вам нужно предоставить серверу доказательство владение). 2. Использование JWT для представления «сеанса» и хранения их в файле cookie для проверки подлинности (который сам по себе является «сеансом») не имеет смысла, я боюсь. 3. XSS не имеет ничего общего с XSRF, это совершенно другая угроза. – Pinpoint

ответ

7

Предлагаю вам взглянуть на следующую ссылку.

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

Они хранят JWT маркер в HTTP только печенье для предотвращения XSS атак.

Они затем проверить маркер JWT в куки, добавив следующий код в Startup.cs:

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, извлекает JWT из файла cookie и добавляет заголовок авторизации на лету, прежде чем он достигнет фильтра Authorize ваших контроллеров. Вот код, который работает для 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); 
     } 
    } 
} 

... где маркер доступ является следующим классом:

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

Надеется, что это помогает.

ПРИМЕЧАНИЕ. Важно также отметить, что этот способ делать вещи (токен в http только cookie) поможет предотвратить атаки XSS, но, тем не менее, не защищает от атак Cross-Site Request Forgery (CSRF), поэтому вы также должны использовать анти-подделки или установить специальные заголовки, чтобы предотвратить их.

Кроме того, если вы не будете заниматься какой-либо санитацией контента, злоумышленник все равно сможет запустить сценарий XSS для выполнения запросов от имени пользователя, даже с включенными только cookie-файлами http и защитой CRSF. Однако злоумышленник не сможет украсть только файлы cookie http, содержащие токены, и злоумышленник не сможет делать запросы с стороннего веб-сайта.

Поэтому вы должны по-прежнему выполнять тяжелую санитарную на пользовательский контент, такие как комментарии и т.д. ...

EDIT: Это было написано в комментариях, что пост в блоге связаны между собой и код были написаны самим О.П. несколько дней назад, задав этот вопрос.

Для тех, кто заинтересован в другом методе «токена в cookie» для уменьшения воздействия XSS, они могут использовать промежуточное ПО OAuth, такое как OpenId Connect Server в ASP.NET Core.

В методе маркеров поставщика, который вызывается для отправки фишки обратно (ApplyTokenResponse()) клиента можно сериализовать маркер и хранить его в печенье, который является HTTP только:

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.Кроме того, необходимо написать несколько межплатформенное перехватить печенье и установить его в заголовок:

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); 
     } 
    } 
} 

В Configure() из startup.cs:

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

Вы можете использовать атрибут Авторизоваться нормально.

[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; 
     } 
    } 
} 
+3

Я должен спросить, вы когда-нибудь проверяли, кто автор этого блога? https://i.ytimg.com/vi/OGAu_DeKckI/hqdefault.jpg – KreepN

+0

Вы делаете очень действительную точку, нет, я не проверял автора. Я рассмотрю более объективное решение. Я сделал некоторый пользовательский эквивалент проверки подлинности с помощью oauth2, я отредактирую это, чтобы предоставить альтернативу. – Darxtar

+0

Lol, я все еще не уверен, что вы заметили: вы связали OP с его собственным сообщением в блоге и кодом. Это все, что я просил. – KreepN

0

Я реализовал промежуточное успешно (Darxtar ответ):

// TokenController.cs 

[AllowAnonymous] 
[HttpGet] 
public IActionResult Get([FromQuery]string username, [FromQuery]string password) 
{ 
    ... 

    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>(); 

    ... 

} 
Смежные вопросы