I am looking for some feedback or guidance. I have built a grpc server and client in dotnet core 3.1. Only this client will be talking to the server, but still need to protect the server endpoints. I am trying to implement authentication security on it and the jwt bearer token seems like a good path. I am trying to follow the example here(which came from this MS Doc) which seems like a pretty simple implementation, however seems insecure for production use since anyone can hit the generate token endpoint. I figured it could modify and implement a shared secret that is passed on the get token request. Does this seem like a good approach or other thoughts?
Besides this part, I have a question about the client side code that I don't quite understand. At the start of the client, it establishes a grpc channel by calling the CreateAuthenticatedChannel() which appears to only add the bearer token if the _token is set. You don't get the token until the client starts up and hit number three, but I don't see where the channel or client is rebuilt to include the metadata token value. The sample appears to work to but don't understand it.
class Program
{
// The port number(5001) must match the port of the gRPC server.
private const string Address = "localhost:5001";
private static string _token;
static async Task Main(string[] args)
{
var channel = CreateAuthenticatedChannel($"https://{Address}");
var client = new Ticketer.TicketerClient(channel);
Console.WriteLine("gRPC Ticketer");
Console.WriteLine();
Console.WriteLine("Press a key:");
Console.WriteLine("1: Get available tickets");
Console.WriteLine("2: Purchase ticket");
Console.WriteLine("3: Authenticate");
Console.WriteLine("4: Exit");
Console.WriteLine();
var exiting = false;
while (!exiting)
{
var consoleKeyInfo = Console.ReadKey(intercept: true);
switch (consoleKeyInfo.KeyChar)
{
case '1':
await GetAvailableTickets(client);
break;
case '2':
await PurchaseTicket(client);
break;
case '3':
_token = await Authenticate();
break;
case '4':
exiting = true;
break;
}
}
Console.WriteLine("Exiting");
}
private static GrpcChannel CreateAuthenticatedChannel(string address)
{
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
if (!string.IsNullOrEmpty(_token))
{
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});
// SslCredentials is used here because this channel is using TLS.
// Channels that aren't using TLS should use ChannelCredentials.Insecure instead.
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
});
return channel;
}
private static async Task<string> Authenticate()
{
Console.WriteLine($"Authenticating as {Environment.UserName}...");
var httpClient = new HttpClient();
var request = new HttpRequestMessage
{
RequestUri = new Uri($"https://{Address}/generateJwtToken?name={HttpUtility.UrlEncode(Environment.UserName)}"),
Method = HttpMethod.Get,
Version = new Version(2, 0)
};
var tokenResponse = await httpClient.SendAsync(request);
tokenResponse.EnsureSuccessStatusCode();
var token = await tokenResponse.Content.ReadAsStringAsync();
Console.WriteLine(token);
Console.WriteLine("Successfully authenticated.");
return token;
}
private static async Task PurchaseTicket(Ticketer.TicketerClient client)
{
Console.WriteLine("Purchasing ticket...");
try
{
var response = await client.BuyTicketsAsync(new BuyTicketsRequest { Count = 1 });
if (response.Success)
{
Console.WriteLine("Purchase successful.");
}
else
{
Console.WriteLine("Purchase failed. No tickets available.");
}
}
catch (Exception ex)
{
Console.WriteLine("Error purchasing ticket." + Environment.NewLine + ex.ToString());
}
}
private static async Task GetAvailableTickets(Ticketer.TicketerClient client)
{
Console.WriteLine("Getting available ticket count...");
var response = await client.GetAvailableTicketsAsync(new Empty());
Console.WriteLine("Available ticket count: " + response.Count);
}
}
}