Otentikasi dalam .NET Core gRpc dengan JWT

Pada artikel ini saya akan berbicara tentang fitur otentikasi API di layanan gRpc menggunakan JWT. Saya berasumsi bahwa Anda terbiasa dengan JWT dan header HTTP yang menggunakannya di .NET Core WebAPI, jadi saya tidak akan membahas detail ini. Ketika saya mencoba menerapkan otentikasi di gRpc, saya menemukan fakta bahwa sebagian besar contoh ditulis menggunakan aplikasi konsol. Ini terlalu jauh dari kenyataan di mana, menurut pendapat saya, pengembang hidup. Misalnya, saya tidak ingin membuat saluran setiap kali saya ingin memanggil metode layanan. Saya juga tidak ingin khawatir tentang mengirim token dan informasi pengguna dengan setiap permintaan. Sebaliknya, saya ingin memiliki tingkat infrastruktur yang akan mengurus semua ini untuk saya. Jika topik ini menarik bagi Anda, maka akan ada lebih banyak di bawah potongan. Semua contoh dalam artikel ini valid untuk .NET Core 3.1.

Contoh yang digunakan


Sebelum mempelajari topik ini, ada baiknya menjelaskan contoh yang digunakan dalam artikel. Seluruh solusi terdiri dari dua aplikasi: situs web dan layanan gRpc (selanjutnya disebut API). Keduanya ditulis dalam .NET Core 3.1. Pengguna dapat login dan melihat beberapa data jika ia diizinkan untuk ini. Situs web tidak menyimpan data pengguna dan bergantung pada API dalam proses otentikasi. Untuk berkomunikasi dengan layanan gRpc, situs web harus memiliki token JWT yang valid, tetapi token ini tidak terkait dengan otentikasi pengguna dalam aplikasi. Aplikasi web menggunakan cookie di sisinya. Agar API mengetahui pengguna mana yang membuat permintaan ke layanan, informasi tentang ini dikirim bersama dengan token JWT, tetapi tidak di token itu sendiri, tetapi dengan header HTTP tambahan. Gambar di bawah ini menunjukkan skema contoh sistem yang baru saja saya bicarakan:


Di sini saya harus mencatat bahwa ketika saya melakukan contoh ini, saya tidak memiliki tujuan menerapkan metode otentikasi yang paling benar untuk API. Jika Anda ingin melihat beberapa praktik terbaik, maka lihat spesifikasi OpenID Connect . Meskipun, kadang-kadang menurut saya solusi yang paling benar bisa menjadi redundan dibandingkan dengan apa yang bisa menyelesaikan masalah dan menghemat waktu dan uang.

Aktifkan Otentikasi JWT di Layanan gRpc


Konfigurasi layanan gRpc tidak berbeda dengan konfigurasi biasa yang diperlukan oleh .NET Core API. Kelebihan lainnya adalah tidak ada perbedaan untuk protokol HTTP dan HTTPS. Secara singkat, Anda perlu menambahkan layanan otentikasi dan otorisasi standar, serta middlewere di file Startup.cs . Tempat Anda menambahkan middleware penting: Anda harus menambahkannya persis antara perutean dan titik ( beberapa kode tidak ada ):

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

Tetapi tempat di mana layanan terdaftar tidak begitu penting, cukup tambahkan metode ConfigureServices () ke metode . Namun di sini Anda perlu mengonfigurasi pemeriksaan token JWT. Ini dapat didefinisikan di sini, tetapi saya sarankan menariknya ke kelas yang terpisah. Dengan demikian, kode tersebut dapat terlihat seperti ini:

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

Kelas JwtTokenValidator adalah kelas di mana Anda akan mendefinisikan logika validasi. Anda perlu membuat kelas TokenValidationParameters dengan pengaturan yang benar dan akan melakukan sisa pekerjaan validasi JWT. Sebagai bonus, Anda dapat menambahkan lapisan keamanan tambahan di sini. Mungkin diperlukan karena JWT adalah format yang terkenal. Jika Anda memiliki JWT, Anda dapat pergi ke jwt.io dan melihat beberapa informasi. Saya lebih suka menambahkan enkripsi tambahan ke JWT, yang membuat dekripsi lebih sulit. Seperti apa bentuk validator:

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

Dan itu semua yang dibutuhkan sisi API. Riwayat pengaturan klien sedikit lebih lama dan sedikit berbeda tergantung pada protokol HTTP atau HTTPS yang dipilih.

Mengirim tajuk HTTP dengan setiap permintaan ke layanan gRpc


Anda mungkin tahu ini dari dokumentasi resmi, yang sebenarnya tidak bisa Anda gunakan di mana pun kecuali dalam program konsol bodoh. Misalnya, Anda bisa melihatnya di sini .

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

Untuk menggunakan ini dalam proyek nyata, kita masih perlu memiliki konfigurasi terpusat dan DI, yang hampir tidak dipertimbangkan. Inilah yang perlu Anda lakukan. Pertama, kita perlu menambahkan paket NuGet yang diperlukan ke proyek kita. Paket Grpc.Tools akan membantu Anda membuat prototipe saat membangun proyek, dan Grpc.Net.ClientFactory akan membantu Anda mengatur DI. Saat bekerja dengan gRpc, jika Anda perlu mengimplementasikan pemrosesan Anda di suatu tempat di tengah-tengah rantai permintaan-respons, Anda perlu menggunakan kelas yang diwarisi dari Interceptor , yang merupakan bagian dari gRpc.Core . Jika Anda perlu mengakses HttpContext.User.Identity di dalam layanan Anda, Anda dapat menambahkan antarmuka IHttpContextAccessor

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



ke layanan Anda (ini memerlukan pendaftaran tambahan di layanan). Anda perlu menambahkan berikut ini ke file Startup.cs Anda.

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

Kelas AuthHeadersInterceptor adalah kelas kita sendiri, berasal dari kelas Interceptor . Ia menggunakan IHttpContextAccessor dan mendaftar .AddHttpContextAccessor () memungkinkan Anda untuk melakukan ini.

Fitur Konfigurasi untuk HTTP


Anda mungkin memperhatikan konfigurasi berikut:

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

Ini diperlukan untuk bekerja melalui HTTP, tetapi ini tidak cukup. Anda juga perlu mengecualikan baris ini dari metode Configure ().

app.UseHttpsRedirection();

Dan Anda masih perlu menari untuk membuat pengaturan khusus sebelum membuat saluran gRpc. Ini dapat dilakukan hanya sekali selama peluncuran aplikasi. Karena itu, saya menambahkannya pada posisi yang hampir sama dengan baris yang dihapus yang disebutkan di atas. Ini seharusnya hanya dipanggil untuk HTTP.

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

Fitur Konfigurasi untuk HTTPS


Ada beberapa kesulitan dengan bekerja dengan SSL di Windows dan Linux. Ini mungkin terjadi bahwa Anda mengembangkan pada komputer Windows dan menyebar ke Docker / Kubernetes menggunakan gambar berbasis Linux. Dalam hal ini, konfigurasi tidak sesederhana yang dijelaskan dalam banyak posting. Saya akan menjelaskan konfigurasi ini di artikel lain, dan di sini saya hanya akan menyentuh kode.

Kita perlu mengkonfigurasi ulang saluran gRpc untuk menggunakan kredensial SSL. Jika Anda menggunakan Docker dan membuat gambar berbasis Linux, Anda mungkin juga perlu mengkonfigurasi HttpClient untuk mengizinkan sertifikat yang tidak valid. HttpClient dibuat untuk setiap saluran.

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

Menambahkan HTTP Header


Header ditambahkan di kelas interseptor (penerus dari Interceptor ). gRpc menggunakan konsep metadata, yang dikirim bersama dengan permintaan sebagai header. Kelas pencegat harus menambahkan metadata untuk konteks panggilan.

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

Untuk skenario, ketika Anda baru saja memanggil layanan gRpc, Anda hanya perlu mengganti metode AsyncUnaryCall . Tentu saja, token JWT dapat disimpan dalam file konfigurasi.

Dan itu semua. Nanti saya akan menambahkan tautan ke kode dengan contoh sederhana dari use case yang dijelaskan. Jika Anda memiliki pertanyaan lebih lanjut, silakan kirim surat kepada saya. Saya akan mencoba menjawab.

All Articles