web-dev-qa-db-fra.com

Comment puis-je définir en toute sécurité le principal d'utilisateur dans un HttpMessageHandler d'API Web personnalisé?

Pour l'authentification de base, j'ai implémenté un HttpMessageHandler personnalisé basé sur l'exemple montré dans la réponse de Darin Dimitrov ici: https://stackoverflow.com/a/11536349/270591

Le code crée une instance principal de type GenericPrincipal avec le nom d'utilisateur et les rôles, puis définit ce principal sur le principal actuel du thread:

Thread.CurrentPrincipal = principal;

Plus loin dans une méthode ApiController, le principal peut être lu en accédant à la propriété User des contrôleurs:

public class ValuesController : ApiController
{
    public void Post(TestModel model)
    {
        var user = User; // this should be the principal set in the handler
        //...
    }
}

Cela semblait bien fonctionner jusqu'à ce que j'ajoute récemment un MediaTypeFormatter personnalisé qui utilise la bibliothèque Task comme ceci:

public override Task<object> ReadFromStreamAsync(Type type, Stream readStream,
    HttpContent content, IFormatterLogger formatterLogger)
{
    var task = Task.Factory.StartNew(() =>
    {
        // some formatting happens and finally a TestModel is returned,
        // simulated here by just an empty model
        return (object)new TestModel();
    });
    return task;
}

(J'ai cette approche pour démarrer une tâche avec Task.Factory.StartNew Dans ReadFromStreamAsync à partir d'un exemple de code. Est-ce mal et peut-être la seule raison du problème?)

Maintenant, "parfois" - et pour moi, il semble être aléatoire - le principal User dans la méthode du contrôleur n'est plus le principal que j'ai défini dans le MessageHandler, c'est-à-dire le nom d'utilisateur, Authenticated le drapeau et les rôles sont tous perdus. La raison semble être que le MediaTypeFormatter personnalisé provoque un changement de thread entre MessageHandler et la méthode du contrôleur. J'ai confirmé cela en comparant les valeurs de Thread.CurrentThread.ManagedThreadId Dans le MessageHandler et dans la méthode du contrôleur. "Parfois", ils sont différents, puis le principal est "perdu".

J'ai maintenant cherché une alternative à la configuration de Thread.CurrentPrincipal Pour transférer le principal en toute sécurité du MessageHandler personnalisé vers la méthode du contrôleur et dans ce billet de blog les propriétés de la requête sont utilisées:

request.Properties.Add(HttpPropertyKeys.UserPrincipalKey,
    new GenericPrincipal(identity, new string[0]));

Je voulais tester cela, mais il semble que la classe HttpPropertyKeys (qui se trouve dans l'espace de noms System.Web.Http.Hosting) N'a plus de propriété UserPrincipalKey dans les dernières versions de WebApi (release candidate ainsi que la version finale de la semaine dernière).

Ma question est la suivante: comment puis-je modifier le dernier extrait de code ci-dessus pour qu'il fonctionne avec la version WebAPI actuelle? Ou généralement: comment puis-je définir le principal d'utilisateur dans un MessageHandler personnalisé et y accéder de manière fiable dans une méthode de contrôleur?

Modifier

Il est mentionné ici que "HttpPropertyKeys.UserPrincipalKey ... se résout en “MS_UserPrincipal”", J'ai donc essayé d'utiliser:

request.Properties.Add("MS_UserPrincipal",
    new GenericPrincipal(identity, new string[0]));

Mais cela ne fonctionne pas comme prévu: la propriété ApiController.User Ne contient pas le principal ajouté à la collection Properties ci-dessus.

50
Slauma

Le problème de la perte du principal sur un nouveau thread est mentionné ici:

http://leastprivilege.com/2012/06/25/important-setting-the-client-principal-in-asp-net-web-api/

Important: définition du client principal dans l'API Web ASP.NET

En raison de certains mécanismes malheureux enfouis profondément dans ASP.NET, la définition de Thread.CurrentPrincipal dans l'hébergement Web d'API Web n'est pas suffisante.

Lors de l'hébergement dans ASP.NET, Thread.CurrentPrincipal peut être remplacé par HttpContext.Current.User lors de la création de nouveaux threads. Cela signifie que vous devez définir le principal à la fois sur le thread et le contexte HTTP.

Et ici: http://aspnetwebstack.codeplex.com/workitem/264

Aujourd'hui, vous devrez définir les deux éléments suivants pour l'utilisateur principal si vous utilisez un gestionnaire de messages personnalisé pour effectuer l'authentification dans le scénario hébergé sur le Web.

IPrincipal principal = new GenericPrincipal(
    new GenericIdentity("myuser"), new string[] { "myrole" });
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;

J'ai ajouté la dernière ligne HttpContext.Current.User = principal (Besoins using System.Web;) au gestionnaire de messages et la propriété User dans le ApiController a toujours le principal correct maintenant, même si le thread a changé en raison de la tâche dans MediaTypeFormatter.

Modifier

Juste pour le souligner: la définition du principal de l'utilisateur actuel de HttpContext n'est nécessaire que lorsque WebApi est hébergé dans ASP.NET/IIS. Pour l'auto-hébergement, ce n'est pas nécessaire (et pas possible car HttpContext est une construction ASP.NET et n'existe pas lorsqu'il est auto-hébergé).

75
Slauma

En utilisant votre MessageHandler personnalisé, vous pouvez ajouter le MS_UserPrincipal propriété en appelant le HttpRequestMessageExtensionMethods.SetUserPrincipal méthode d'extension définie dans System.ServiceModel.Channels:

protected override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    var user = new GenericPrincipal(new GenericIdentity("UserID"), null);
    request.SetUserPrincipal(user);
    return base.SendAsync(request, cancellationToken);
}

Notez que cela n'ajoute que cette propriété à la collection Propriétés de la demande, cela ne change pas l'utilisateur attaché à ApiController.

5
gmoody1979

Pour éviter le changement de contexte, essayez d'utiliser un TaskCompletionSource<object> au lieu de démarrer manuellement une autre tâche dans votre MediaTypeFormatter personnalisé:

public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
    var tcs = new TaskCompletionSource<object>();

    // some formatting happens and finally a TestModel is returned,
    // simulated here by just an empty model
    var testModel = new TestModel();

    tcs.SetResult(testModel);
    return tcs.Task;
}
5
Darin Dimitrov