2016-05-23 10 views
7

Oprogramowanie pośredniczące w ASP.NET Core w wersji UseJwtBearerAuthentication ułatwia sprawdzanie nadchodzących tokenów internetowych JSON w nagłówkach Authorization.W jaki sposób mogę sprawdzić ważność JWT przekazywanego za pośrednictwem plików cookie?

Jak mogę uwierzytelnić JWT przekazywane za pomocą plików cookie, zamiast nagłówka? Coś takiego jak UseCookieAuthentication, ale w przypadku pliku cookie, który zawiera właśnie JWT.

+2

Ciekawy: jaki jest sens używania tokenów na okaziciela, jeśli chcesz używać plików cookie do ich przepływu? Cały sens używania znaczników okaziciela zamiast ciasteczek ma na celu uniknięcie problemów związanych z bezpieczeństwem, takich jak ataki XSRF. Jeśli ponownie wprowadzisz pliki cookie w równaniu, ponownie wprowadzisz swój model zagrożenia. – Pinpoint

+1

@Pinpoint JWT nie są tożsami naznaczonymi; mogą być używane albo za pomocą nagłówka nośnika, albo za pomocą plików cookie. Używam JWTs do bezpaństwowych "sesji", ale nadal przechowuję je w ciasteczkach, ponieważ obsługa przeglądarki jest prosta. XSS jest łagodzony przez flagi plików cookie. –

+0

1. Z definicji JWT są to żetony na okaziciela lub PoP (w pierwszym przypadku nie trzeba udowadniać, że jesteś prawowitym posiadaczem tokena, w drugim musisz dać serwerowi dowód posiadanie). 2. Obawiam się, że używanie JWT do reprezentowania "sesji" i przechowywanie ich w pliku cookie uwierzytelniania (który sam jest "sesją") nie ma sensu. 3. XSS nie ma nic wspólnego z XSRF, to zupełnie inne zagrożenie. – Pinpoint

Odpowiedz

7

Proponuję zapoznać się z poniższym linkiem.

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

one przechowywać JWT żeton w http tylko ciasteczko, aby zapobiec atakom XSS.

one następnie zweryfikować tokenu JWT w pliku cookie, dodając następujący kod w Startup.cs:

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

Gdzie CustomJwtDataFormat() jest ich format niestandardowy zdefiniowany tutaj:

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

Innym rozwiązaniem byłoby napisanie niestandardowego oprogramowania pośredniego, które przechwyciłoby każde żądanie, sprawdzałoby, czy ma plik cookie, wyodrębnij JWT z pliku cookie i dodaj nagłówek Authorization w locie, zanim dotrze do filtra autoryzacji kontrolerów. Oto kod, który działa na tokeny OAuth, aby dostać się pomysł:

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

... gdzie AccessToken jest następujące klasy:

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

Nadzieja to pomaga.

UWAGA: Ważne jest również, aby pamiętać, że ten sposób robienia rzeczy (token w ciasteczku http) pomaga zapobiegać atakom XSS, ale nie jest odporny na ataki Cross Site Request Forgery (CSRF), dlatego też należy tokeny przeciwdziałające fałszerstwom lub ustaw niestandardowe nagłówki, aby temu zapobiec.

Co więcej, jeśli nie przeprowadzasz żadnej dezynfekcji zawartości, osoba atakująca może nadal uruchamiać skrypt XSS, aby wysyłać żądania w imieniu użytkownika, nawet jeśli włączone są tylko pliki cookie z włączoną obsługą http i ochroną CRSF. Jednak osoba atakująca nie będzie w stanie wykraść ciasteczek zawierających tylko tokenów, a osoba atakująca nie będzie mogła wysyłać próśb ze strony osób trzecich.

dlatego należy jeszcze wykonać ciężką odkażenie na treści generowane przez użytkowników, takich jak komentarze itp ...

EDIT: To zostało napisane w komentarzach, które powiązane blogu, a kod został napisany przez samego OP kilka dni temu po zadaniu tego pytania.

Dla osób, które są zainteresowane innym podejściem "token in a cookie" w celu zmniejszenia ekspozycji XSS, mogą korzystać z oprogramowania pośredniczącego oAuth, takiego jak serwer OpenId Connect w środowisku ASP.NET Core.

W sposobie token dostawcy, który jest wykonywany w celu wysłania żeton (ApplyTokenResponse()) do klienta można szeregować token i zapisać go w pliku cookie, który jest http tylko:

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

Następnie upewnij się, że każde żądanie ma dołączony plik cookie.Musisz również napisać middleware przechwytywać cookie i ustaw go w nagłówku:

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

W Configure() z startup.cs:

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

Następnie można użyć atrybutu Autoryzuj normalnie.

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

To rozwiązanie działa zarówno w aplikacjach api, jak i mvc. Ajax i pobrać wnioski jednak swoje trzeba napisać niestandardowy middleware, że nie będzie przekierować użytkownika na stronę logowania i zamiast zwracać 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

Muszę zapytać, czy kiedykolwiek sprawdzałeś, kto jest autorem tego bloga? https://i.ytimg.com/vi/OGAu_DeKckI/hqdefault.jpg – KreepN

+0

Tworzysz bardzo ważny punkt, nie, nie sprawdziłem autora. Przyjrzę się bardziej obiektywnemu rozwiązaniu. Zrobiłem niestandardowy odpowiednik sprawdzania autentyczności za pomocą oauth2, wkrótce dokonam edycji, aby zapewnić alternatywę. – Darxtar

+0

Lol, nadal nie jestem pewien, czy zauważyłeś: połączyłeś OP z własnym blogiem i kodem. To wszystko, o co prosiłem. – KreepN

0

I wdrożone middleware powodzeniem (Darxtar odpowiedź):

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

    ... 

} 
Powiązane problemy