web-dev-qa-db-fra.com

Cookie ASP.NET Core WebAPI + authentification JWT

nous avons un SPA (angulaire) avec backend API (ASP.NET Core WebAPI):

SPA écoute sur app.mydomain.com, API sur app.mydomain.com/API

Nous utilisons JWT pour l'authentification avec Microsoft.AspNetCore.Authentication.JwtBearer Intégré; J'ai un contrôleur app.mydomain.com/API/auth/jwt/login Qui crée des jetons. SPA les enregistre dans le stockage local. Tout fonctionne parfaitement. Après un audit de sécurité, on nous a dit de changer de stockage local pour les cookies.

Le problème est que cette API sur app.mydomain.com/API Est utilisée par SPA mais aussi par une application mobile et plusieurs solutions serveur-2-clients.

Nous devons donc conserver JWT tel quel, mais ajouter des cookies. J'ai trouvé plusieurs articles qui combinent les cookies et JWT sur différents contrôleurs, mais j'ai besoin qu'ils fonctionnent côte à côte sur chaque contrôleur.

Si le client envoie des cookies, authentifiez-vous via les cookies. Si le client envoie un porteur JWT, authentifiez-vous via JWT.

Est-ce réalisable via l'authentification ASP.NET intégrée ou un middleware DIY?

Merci!

6
Luke1988

J'ai eu le même problème et je viens de trouver ce que cela semble être la solution dans une autre question ici dans stackoverflow.

Veuillez jeter un œil à this .

Je vais essayer cette solution moi-même et mettre à jour cette réponse avec les résultats.

Edit: Il semble qu'il ne soit pas possible d'obtenir des types d'authentification double dans une même méthode, mais la solution fournie dans le lien que j'ai mentionné dit:

Il n'est pas possible d'autoriser une méthode avec deux schémas Or-Like, mais vous pouvez utiliser deux méthodes publiques pour appeler une méthode privée

//private method
private IActionResult GetThingPrivate()
{
   //your Code here
}
//Jwt-Method
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme}")]
[HttpGet("bearer")]
public IActionResult GetByBearer()
{
   return GetThingsPrivate();
}
 //Cookie-Method
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme}")]
[HttpGet("cookie")]
public IActionResult GetByCookie()
{
   return GetThingsPrivate();
}    

Quoi qu'il en soit, vous devriez jeter un œil au lien, cela m'a certainement aidé. Nous remercions Nikolaus pour la réponse.

4

Je n'ai pas pu trouver beaucoup d'informations sur un bon moyen de le faire - avoir à dupliquer l'API est une douleur juste pour prendre en charge 2 schémas d'autorisation.

J'ai étudié l'idée d'utiliser un proxy inverse et cela me semble être une bonne solution pour cela.

  1. L'utilisateur se connecte au site Web (utilisez le cookie http uniquement pour la session)
  2. Le site Web utilise un jeton anti-contrefaçon
  3. Le SPA envoie une demande au serveur du site Web et inclut un jeton anti-contrefaçon dans l'en-tête: https://app.mydomain.com/api/secureResource
  4. Le serveur de site Web vérifie le jeton anti-contrefaçon (CSRF)
  5. Le serveur du site Web détermine que la demande concerne l'API et doit l'envoyer au proxy inverse
  6. Le serveur de site Web obtient des utilisateurs un jeton d'accès pour l'API
  7. Le proxy inverse transmet la demande à l'API: https://api.mydomain.com/api/secureResource

Notez que le jeton anti-contrefaçon (# 2, # 4) est critique, sinon vous pourriez exposer votre API aux attaques CSRF.


Exemple (MVC .NET Core 2.1 avec IdentityServer4):

Pour obtenir un exemple de travail, j'ai commencé avec le démarrage rapide d'IdentityServer4 Basculer vers le flux hybride et ajouter à nouveau l'accès à l'API . Cela met en place le scénario que j'ai recherché après lequel une application MVC utilise des cookies et peut demander un access_token au serveur d'identité pour appeler l'API.

J'ai utilisé Microsoft.AspNetCore.Proxy pour le proxy inverse et modifié le démarrage rapide.

MVC Startup.ConfigureServices:

services.AddAntiforgery();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

MVC Startup.Configure:

app.MapWhen(IsApiRequest, builder =>
{
    builder.UseAntiforgeryTokens();

    var messageHandler = new BearerTokenRequestHandler(builder.ApplicationServices);
    var proxyOptions = new ProxyOptions
    {
        Scheme = "https",
        Host = "api.mydomain.com",
        Port = "443",
        BackChannelMessageHandler = messageHandler
    };
    builder.RunProxy(proxyOptions);
});

private static bool IsApiRequest(HttpContext httpContext)
{
    return httpContext.Request.Path.Value.StartsWith(@"/api/", StringComparison.OrdinalIgnoreCase);
}

ValidateAntiForgeryToken (Marius Schulz):

public class ValidateAntiForgeryTokenMiddleware
{
    private readonly RequestDelegate next;
    private readonly IAntiforgery antiforgery;

    public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
    {
        this.next = next;
        this.antiforgery = antiforgery;
    }

    public async Task Invoke(HttpContext context)
    {
        await antiforgery.ValidateRequestAsync(context);
        await next(context);
    }
}

public static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder UseAntiforgeryTokens(this IApplicationBuilder app)
    {
        return app.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
    }
}

BearerTokenRequestHandler:

public class BearerTokenRequestHandler : DelegatingHandler
{
    private readonly IServiceProvider serviceProvider;

    public BearerTokenRequestHandler(IServiceProvider serviceProvider, HttpMessageHandler innerHandler = null)
    {
        this.serviceProvider = serviceProvider;
        InnerHandler = innerHandler ?? new HttpClientHandler();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
        var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
        request.Headers.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
        var result = await base.SendAsync(request, cancellationToken);
        return result;
    }
}

_Layout.cshtml:

@Html.AntiForgeryToken()

Ensuite, en utilisant votre framework SPA, vous pouvez faire une demande. Pour vérifier, je viens de faire une simple demande AJAX:

<a onclick="sendSecureAjaxRequest()">Do Secure AJAX Request</a>
<div id="ajax-content"></div>

<script language="javascript">
function sendSecureAjaxRequest(path) {
    var myRequest = new XMLHttpRequest();
    myRequest.open('GET', '/api/secureResource');
    myRequest.setRequestHeader("RequestVerificationToken",
        document.getElementsByName('__RequestVerificationToken')[0].value);
    myRequest.onreadystatechange = function () {
        if (myRequest.readyState === XMLHttpRequest.DONE) {
            if (myRequest.status === 200) {
                document.getElementById('ajax-content').innerHTML = myRequest.responseText;
            } else {
                alert('There was an error processing the AJAX request: ' + myRequest.status);
            }
        }  
    };
    myRequest.send();
};
</script>

C'était un test de preuve de concept, donc votre kilométrage peut très bien et je suis assez nouveau dans la configuration de .NET Core et du middleware afin qu'il puisse probablement avoir l'air plus joli. J'ai fait des tests limités avec cela et n'ai fait qu'une demande GET à l'API et n'ai pas utilisé SSL (https).

Comme prévu, si le jeton anti-contrefaçon est supprimé de la demande AJAX, il échoue. Si l'utilisateur n'est pas connecté (authentifié), la demande échoue.

Comme toujours, chaque projet est unique, vérifiez donc toujours que vos exigences de sécurité sont respectées. Veuillez consulter tous les commentaires laissés sur cette réponse pour tout problème de sécurité potentiel que quelqu'un pourrait soulever.

Sur une autre note, je pense qu'une fois l'intégrité des sous-ressources (SRI) et la politique de sécurité du contenu (CSP) disponibles sur tous les navigateurs couramment utilisés (c'est-à-dire que les anciens navigateurs sont supprimés), le stockage local devrait être réévalué pour stocker les jetons d'API qui va apprendre la complexité du stockage de jetons. Le SRI et le CSP devraient être utilisés maintenant pour aider à réduire la surface d'attaque des navigateurs compatibles.

3
Mike Rowley