Authentification dans .NET Core gRpc avec JWT

Dans cet article, je parlerai des fonctionnalitĂ©s de l'authentification API dans les services gRpc utilisant JWT. Je suppose que vous connaissez les en-tĂȘtes JWT et HTTP qui les utilisent dans .NET Core WebAPI, donc je ne discuterai pas de ces dĂ©tails. Lorsque j'ai essayĂ© d'implĂ©menter l'authentification dans gRpc, je suis tombĂ© sur le fait que la plupart des exemples sont Ă©crits Ă  l'aide d'applications console. C'est trop loin de la rĂ©alitĂ© dans laquelle vivent, Ă  mon avis, les dĂ©veloppeurs. Par exemple, je ne souhaite pas crĂ©er de chaĂźne Ă  chaque fois que je souhaite appeler une mĂ©thode de service. Je ne veux pas non plus me soucier d'envoyer un jeton et des informations utilisateur Ă  chaque demande. Au lieu de cela, je veux avoir un niveau d'infrastructure qui s'occupera de tout cela pour moi. Si ce sujet vous intĂ©resse, il y en aura plus sous la coupe. Tous les exemples de cet article sont valides pour .NET Core 3.1.

Exemple utilisé


Avant de plonger dans le sujet, il convient de dĂ©crire l'exemple utilisĂ© dans l'article. L'ensemble de la solution se compose de deux applications: un site Web et un service gRpc (ci-aprĂšs API). Les deux sont Ă©crits en .NET Core 3.1. L'utilisateur peut se connecter et voir certaines donnĂ©es s'il y est autorisĂ©. Le site Web ne stocke pas de donnĂ©es utilisateur et s'appuie sur une API dans le processus d'authentification. Pour communiquer avec le service gRpc, le site Web doit avoir un jeton JWT valide, mais ce jeton ne s'applique pas Ă  l'authentification des utilisateurs dans l'application. L'application Web utilise des cookies de son cĂŽtĂ©. Pour que l'API sache quel utilisateur fait la demande au service, des informations Ă  ce sujet sont envoyĂ©es avec le jeton JWT, mais pas dans le jeton lui-mĂȘme, mais avec un en-tĂȘte HTTP supplĂ©mentaire. La figure ci-dessous montre un exemple de schĂ©ma du systĂšme dont je viens de parler:


Ici, je dois noter que lorsque j'ai fait cet exemple, je n'avais pas pour objectif de mettre en Ɠuvre la mĂ©thode d'authentification la plus correcte pour l'API. Si vous souhaitez voir certaines des meilleures pratiques, consultez la spĂ©cification OpenID Connect . Bien que, parfois, il me semble que la solution la plus correcte peut ĂȘtre redondante par rapport Ă  ce qui peut rĂ©soudre le problĂšme et Ă©conomiser du temps et de l'argent.

Activer l'authentification JWT dans le service gRpc


La configuration du service gRpc n'est pas diffĂ©rente de la configuration habituelle requise par l'API .NET Core. Un avantage supplĂ©mentaire est qu'il n'est pas diffĂ©rent pour les protocoles HTTP et HTTPS. En bref, vous devez ajouter des services d'authentification et d'autorisation standard, ainsi que des middlewere dans le fichier Startup.cs . L'endroit oĂč vous ajoutez le middleware est important: vous devez l'ajouter exactement entre le routage et les points ( il manque du code ):

public void Configure(...) {
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.UseEndpoints(...
}

Mais l'endroit oĂč les services sont enregistrĂ©s n'est pas si important, ajoutez simplement la mĂ©thode ConfigureServices () Ă  la mĂ©thode . Mais ici, vous devez configurer la vĂ©rification du jeton JWT. Il peut ĂȘtre dĂ©fini ici, mais je recommande de le retirer dans une classe distincte. Ainsi, le code peut ressembler Ă  ceci:

public void ConfigureServices(...) {
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(o => {
            var validator = new JwtTokenValidator(...);
            o.SecurityTokenValidators.Add(validator);
        });
    services.AddAuthorization();
}

La classe JwtTokenValidator est celle oĂč vous dĂ©finirez la logique de validation. Vous devez crĂ©er la classe TokenValidationParameters avec les paramĂštres corrects et elle fera le reste du travail de validation JWT. En prime, vous pouvez ajouter une couche de sĂ©curitĂ© supplĂ©mentaire ici. Il peut ĂȘtre nĂ©cessaire car JWT est un format bien connu. Si vous avez JWT, vous pouvez aller sur jwt.io et voir quelques informations. Je prĂ©fĂšre ajouter un chiffrement supplĂ©mentaire au JWT, ce qui rend le dĂ©chiffrement plus difficile. Voici Ă  quoi pourrait ressembler un validateur:

public class JwtTokenValidator : ISecurityTokenValidator
{
    public bool CanReadToken(string securityToken) => true;
    
    public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
    {
        var handler = new JwtSecurityTokenHandler();
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "your string",
            ValidAudience = "your string",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your secrete code"))
        };
        
        var claimsPrincipal = handler.ValidateToken(token, tokenValidationParameters, out validatedToken);
        return claimsPrincipal;
    }
    
    public bool CanValidateToken { get; } = true;
    public int MaximumTokenSizeInBytes { get; set; } = int.MaxValue;
}

Et c'est tout ce dont l'API a besoin. L'historique de configuration du client est légÚrement plus long et légÚrement différent selon le protocole HTTP ou HTTPS sélectionné.

Envoi d'en-tĂȘtes HTTP avec chaque demande au service gRpc


Vous le connaissez peut-ĂȘtre dans la documentation officielle, que vous ne pouvez en fait utiliser nulle part sauf dans un programme de console stupide. Par exemple, vous pouvez le voir ici .

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
    new HelloRequest { Name = "World" });
Console.WriteLine(response.Message);

Pour l'utiliser dans un vrai projet, nous avons encore besoin d'avoir une configuration centralisĂ©e et DI, qui ne sont presque pas considĂ©rĂ©s. Voici ce que tu dois faire. Tout d'abord, nous devons ajouter les packages NuGet nĂ©cessaires Ă  notre projet. Le package Grpc.Tools vous aidera Ă  crĂ©er des prototypes lors de la construction d'un projet, et Grpc.Net.ClientFactory vous aidera Ă  configurer DI. Lorsque vous travaillez avec gRpc, si vous devez implĂ©menter votre traitement quelque part au milieu de la chaĂźne de requĂȘte-rĂ©ponse, vous devez utiliser des classes hĂ©ritĂ©es d' Interceptor , qui fait partie de gRpc.Core . Si vous devez accĂ©der Ă  HttpContext.User.Identity au sein de vos services, vous pouvez ajouter l'interface IHttpContextAccessor

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
dotnet add package Grpc.Net.ClientFactory



à votre service (cela nécessite une inscription supplémentaire dans les services). Vous devez ajouter ce qui suit à votre fichier Startup.cs.

services.AddTransient<AuthHeadersInterceptor>();
services.AddHttpContextAccessor();

var httpClientBuilder = services.AddGrpcClient<MygRpcService.MygRpcServiceClient>(o => { o.Address = new Uri("grpc-endpoint-url"); });
httpClientBuilder.AddInterceptor<AuthHeadersInterceptor>();              
httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

La classe AuthHeadersInterceptor est notre propre classe, dérivée de la classe Interceptor . Il utilise IHttpContextAccessor et l'enregistrement .AddHttpContextAccessor () vous permet de le faire.

Fonctions de configuration pour HTTP


Vous pouvez remarquer la configuration suivante:

httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

Il est nécessaire pour travailler via HTTP, mais ce n'est pas suffisant. Vous devez également exclure cette ligne de la méthode Configure ().

app.UseHttpsRedirection();

Et encore, vous devez danser pour Ă©tablir un rĂ©glage spĂ©cial avant de crĂ©er un canal gRpc. Cela ne peut ĂȘtre effectuĂ© qu'une seule fois lors du lancement de l'application. Par consĂ©quent, je l'ai ajoutĂ© Ă  peu prĂšs au mĂȘme endroit que la ligne supprimĂ©e mentionnĂ©e ci-dessus. Cela ne devrait ĂȘtre appelĂ© que pour HTTP.

AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

Caractéristiques de configuration pour HTTPS


Il y a quelques difficultés à travailler avec SSL sur Windows et Linux. Il peut arriver que vous développiez sur un ordinateur Windows et déployiez sur Docker / Kubernetes à l'aide d'images basées sur Linux. Dans ce cas, la configuration n'est pas aussi simple que celle décrite dans de nombreux articles. Je décrirai cette configuration dans un autre article, et ici je ne toucherai qu'au code.

Nous devons reconfigurer le canal gRpc pour utiliser les informations d'identification SSL. Si vous dĂ©ployez sur Docker et crĂ©ez des images basĂ©es sur Linux, vous devrez peut-ĂȘtre Ă©galement configurer HttpClient pour autoriser les certificats non valides. HttpClient est crĂ©Ă© pour chaque canal.

httpClientBuilder.ConfigureChannel(o =>
{
    // add SSL credentials
    o.Credentials = new SslCredentials();
    // allow invalid/untrusted certificates
    var httpClientHandler = new HttpClientHandler
    {
        ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    };
    var httpClient = new HttpClient(httpClientHandler);
    o.HttpClient = httpClient;
});

Ajout d'en-tĂȘtes HTTP


Les en-tĂȘtes sont ajoutĂ©s dans la classe d'intercepteur (successeur d' Interceptor ). gRpc utilise le concept de mĂ©tadonnĂ©es, qui est envoyĂ© avec les demandes comme en-tĂȘtes. La classe d'intercepteur doit ajouter des mĂ©tadonnĂ©es pour le contexte d'appel.

public class AuthHeadersInterceptor : Interceptor
{
    public AuthHeadersInterceptor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var metadata = new Metadata
        {
            {HttpHeaderNames.Authorization, $"Bearer <JWT_TOKEN>"}
        };
        var userIdentity = _httpContextAccessor.HttpContext.User.Identity;
        if (userIdentity.IsAuthenticated)
        {
            metadata.Add(HttpHeaderNames.User, userIdentity.Name);
        }
        var callOption = context.Options.WithHeaders(metadata);
        context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context.Host, callOption);
        
        return base.AsyncUnaryCall(request, context, continuation);
    }
}

Pour le scĂ©nario, lorsque vous appelez simplement le service gRpc, vous devez uniquement remplacer la mĂ©thode AsyncUnaryCall . Bien sĂ»r, le jeton JWT peut ĂȘtre enregistrĂ© dans des fichiers de configuration.

Et c'est tout. Plus tard, j'ajouterai un lien vers le code avec un exemple simple du cas d'utilisation décrit. Si vous avez d'autres questions, écrivez-moi. Je vais essayer de répondre.

All Articles