Cysharp / MagicOnion

Unified Realtime/API framework for .NET platform and Unity.
MIT License
3.89k stars 433 forks source link

[bug] JWT Authentication fails on second request despite token not being expired #812

Open phields opened 4 months ago

phields commented 4 months ago

Hello,

I've encountered an issue with JWT authentication in the gRPC client demo. The problem occurs when making multiple requests using the same IGreeterService client. Here's a detailed description of the issue:

  1. The first call to greeterClient.HelloAsync() succeeds and authenticates correctly.
  2. Any subsequent calls to greeterClient.HelloAsync() fail with a 401 Unauthorized error.
  3. This happens even though the JWT token has not expired (I've verified this by logging the token and its expiration time).
  4. The only way to make a successful second call is to create a new MagicOnionClient instance before each HelloAsync() call.

Here's a simplified version of the code that demonstrates the issue:

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var greeterClient = MagicOnionClient.Create<IGreeterService>(channel, new[] { new WithAuthenticationFilter(signInId, password, channel), });

// This call succeeds
Console.WriteLine($"[IGreeterService.HelloAsync] {await greeterClient.HelloAsync()}");

// This call fails with 401 Unauthorized
Console.WriteLine($"[IGreeterService.HelloAsync] {await greeterClient.HelloAsync()}");

// Creating a new client for each call works, but seems inefficient
var newGreeterClient = MagicOnionClient.Create<IGreeterService>(channel, new[] { new WithAuthenticationFilter(signInId, password, channel), });
Console.WriteLine($"[IGreeterService.HelloAsync] {await newGreeterClient.HelloAsync()}");

Could you please advise on what might be causing this behavior and how to correctly handle multiple authenticated requests using the same client instance?

licentia88 commented 1 month ago

Hi, could you provide a sample project or share your authentication filter, along with how you're verifying your tokens? I don't think your issue is related to MagicOnion. I'm using LitJWT (you can find it here: https://github.com/Cysharp/LitJWT) and have successfully verified my tokens each time.

You can find my implementation in the following repository: https://github.com/licentia88/MagicOnionGenericTemplate

mayuki commented 1 month ago

I'm very sorry, but it seems that there is a bug in the sample code. If you fix WithAuthenticationFilter.SendAsync as follows, it will work.

public async ValueTask<ResponseContext> SendAsync(RequestContext context, Func<RequestContext, ValueTask<ResponseContext>> next)
{
    if (AuthenticationTokenStorage.Current.IsExpired)
    {
        Console.WriteLine($@"[WithAuthenticationFilter/IAccountService.SignInAsync] Try signing in as '{_signInId}'... ({(AuthenticationTokenStorage.Current.Token == null ? "FirstTime" : "RefreshToken")})");

        var client = MagicOnionClient.Create<IAccountService>(_channel);
        var authResult = await client.SignInAsync(_signInId, _password);
        if (!authResult.Success)
        {
            throw new Exception("Failed to sign-in on the server.");
        }
        Console.WriteLine($@"[WithAuthenticationFilter/IAccountService.SignInAsync] User authenticated as {authResult.Name} (UserId:{authResult.UserId})");

        AuthenticationTokenStorage.Current.Update(authResult.Token, authResult.Expiration); // NOTE: You can also read the token expiration date from JWT.

        if (context.CallOptions.Headers?.FirstOrDefault(x => string.Equals(x.Key, "Authorization", StringComparison.OrdinalIgnoreCase)) is {} entry)
        {
            context.CallOptions.Headers?.Remove(entry);
        }
    }

    if (!context.CallOptions.Headers?.Any(x => string.Equals(x.Key, "Authorization", StringComparison.OrdinalIgnoreCase)) ?? false)
    {
        context.CallOptions.Headers?.Add("Authorization", "Bearer " + AuthenticationTokenStorage.Current.Token);
    }

    return await next(context);
}