2016-05-23 23 views
7

Il middleware UseJwtBearerAuthentication in ASP.NET Core facilita la convalida dei token Web JSON in entrata nelle intestazioni Authorization.Come posso convalidare un JWT passato tramite i cookie?

Come si autentica un JWT inoltrato tramite cookie, anziché un'intestazione? Qualcosa come UseCookieAuthentication, ma per un cookie che contiene solo un JWT.

+2

Curioso: che senso ha usare i token bearer se si desidera utilizzare i cookie per trasferirli? L'intero scopo dell'utilizzo di token al portatore invece dei cookie è di evitare problemi di sicurezza come gli attacchi XSRF. Se si reintroducono i cookie nell'equazione, si reintroduce il proprio modello di minaccia. – Pinpoint

+1

@Pinpoint I JWT non sono token strettamente portanti; possono essere utilizzati sia tramite un'intestazione Bearer, o tramite cookie. Sto usando i JWT per fare "sessioni" stateless, ma li sto ancora salvando nei cookies perché il supporto del browser è semplice. XSS è mitigato dalle flag dei cookie. –

+0

1. Per definizione, le JWT sono o token al portatore o PoP (nel primo caso, non è necessario dimostrare di essere un legittimo proprietario del token, nel secondo caso, è necessario fornire al server una prova di possesso). 2. L'uso di JWT per rappresentare una "sessione" e archiviarli in un cookie di autenticazione (che è di per sé una "sessione") non ha senso, temo. 3. XSS non ha nulla a che fare con XSRF, è una minaccia completamente diversa. – Pinpoint

risposta

7

Ti suggerisco di dare un'occhiata al seguente link.

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

immagazzinano JWT gettone in un HTTP solo biscotto per prevenire attacchi XSS.

Hanno quindi convalidare il token JWT nel cookie aggiungendo il seguente codice nel Startup.cs:

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

Dove CustomJwtDataFormat() è il loro formato personalizzato definito qui:

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

altro la soluzione sarebbe scrivere del middleware personalizzato che intercetti ogni richiesta, controllare se ha un cookie, estrarre il JWT dal cookie e aggiungere al volo un'intestazione di autorizzazione prima che raggiunga il filtro Autorizza dei controller. Ecco un po 'di codice che funziona per i token OAuth, per avere l'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); 
     } 
    } 
} 

... dove access token è la seguente classe:

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

Spero che questo aiuti.

NOTA: È anche importante notare che questo modo di fare le cose (token in cookie solo http) aiuta a prevenire gli attacchi XSS ma comunque non è immune dagli attacchi CSRF (Cross Site Request Forgery), pertanto è necessario gettoni anti-contraffazione o impostare intestazioni personalizzate per impedire quelli.

Inoltre, se non si esegue alcuna sanificazione dei contenuti, un utente malintenzionato può comunque eseguire uno script XSS per effettuare richieste per conto dell'utente, anche con i cookie solo http e la protezione CRSF abilitata. Tuttavia, l'autore dell'attacco non sarà in grado di rubare i cookie solo http che contengono i token, né l'attaccante sarà in grado di effettuare richieste da un sito Web di terzi.

Si dovrebbe quindi ancora eseguire sanificazione pesante su contenuti generati dagli utenti, come i commenti, ecc ...

EDIT: E 'stato scritto nei commenti che il post sul blog collegato e il codice sono state scritte dal PO se stesso pochi giorni fa dopo aver fatto questa domanda.

Per coloro che sono interessati a un altro approccio "token in a cookie" per ridurre l'esposizione XSS, possono utilizzare il middleware oAuth come OpenId Connect Server in ASP.NET Core.

Nel metodo del provider di token che viene richiamato per inviare il token (ApplyTokenResponse()) al client è possibile serializzare il token e conservarla in un cookie che è http solo:

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

Quindi devi assicurarti che ogni richiesta contenga il cookie.È inoltre necessario scrivere del middleware per intercettare il cookie e impostarlo l'intestazione:

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

In Configura() di startup.cs:

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

È quindi possibile utilizzare l'attributo Autorizza normalmente.

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

Questa soluzione funziona sia per app api che per mvc. Ajax e prendere richieste comunque il vostro deve scrivere del middleware personalizzato che non reindirizzare l'utente alla pagina di login e invece restituire un 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

Devo chiedere, hai mai controllato chi era l'autore di quel blog? https://i.ytimg.com/vi/OGAu_DeKckI/hqdefault.jpg – KreepN

+0

Hai fatto un punto molto valido, no non ho controllato l'autore. Cercherò una soluzione più obiettiva. Ho realizzato alcuni equivalenti di autenticazione dell'autore personalizzati usando oauth2, lo modificherò presto per fornire un'alternativa. – Darxtar

+0

Lol, non sono ancora sicuro di aver notato: hai collegato l'OP al suo blog post e al codice. Questo è tutto ciò che stavo chiedendo. – KreepN

0

ho implementato il middleware con successo (Darxtar risposta):

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

    ... 

} 
Problemi correlati