I’m not really skilled in C# but I need to create a code snippet for another software that gets events from a Google Calendar. This software is very limited in the library collection so I can use only a few of them (no JWT libraries)
I’m manually creating it but when I check it on https://jwt.io I always get invalid signature, can’t really understand what I’m doing wrong. I can’t understand if the issue is in the key formatting or if I’m messing with the creation of the JWT.
p.s. you can safely ignore if the usage of classes/main is wrong, I’ll just use a single function in the software so I added some main/class just to try it on Visual Studio.
This is the actual code that I’m trying:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
class Program
{
static async Task<string> CheckEventAtCurrentTime()
{
// Define service account details
string serviceAccountEmail = "[email protected]";
string privateKey = @"-----BEGIN PRIVATE KEY-----
my
private
key
-----END PRIVATE KEY-----";
string tokenUrl = "https://oauth2.googleapis.com/token";
string scope = "https://www.googleapis.com/auth/calendar.readonly";
// Create the JWT header
var header = new { alg = "RS256", typ = "JWT" };
string headerEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header)))
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
// Create the JWT payload
long iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
long exp = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds(); // Token expires in 1 hour
var payload = new
{
iss = serviceAccountEmail,
scope = scope,
aud = tokenUrl,
exp = exp,
iat = iat
};
string payloadEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload)))
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
// Concatenate header and payload
string unsignedToken = $"{headerEncoded}.{payloadEncoded}";
Console.WriteLine($"Unsigned JWT: {unsignedToken}");
// Extract the Base64 part of the PEM
string extractedPrivateKey = privateKey
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Replace("n", "").Replace("r", "");
// Remove any non-base64 characters (newlines, etc.)
extractedPrivateKey = Regex.Replace(extractedPrivateKey, @"s+", ""); // Removes all whitespace, newlines, etc.
// Decode the Base64 private key
byte[] privateKeyBytes = Convert.FromBase64String(extractedPrivateKey);
// Import the key into an RSA object
using (var rsa = RSA.Create())
{
rsa.ImportPkcs8PrivateKey(new ReadOnlySpan<byte>(privateKeyBytes), out _);
// Create the RSA signature
byte[] signedBytes = rsa.SignData(Encoding.UTF8.GetBytes(unsignedToken), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
string signedToken = Convert.ToBase64String(signedBytes)
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
// The final JWT
string jwt = $"{unsignedToken}.{signedToken}";
Console.WriteLine($"Generated JWT: {jwt}");
// Request OAuth2 token
using (var httpClient = new HttpClient())
{
var requestBody = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
new KeyValuePair<string, string>("assertion", jwt)
});
var response = await httpClient.PostAsync(tokenUrl, requestBody);
if (!response.IsSuccessStatusCode)
{
return "1"; // API call failed
}
var responseBody = await response.Content.ReadAsStringAsync();
dynamic tokenResponse = JsonConvert.DeserializeObject(responseBody);
string accessToken = tokenResponse.access_token;
// Set the calendar ID and time range
string calendarId = "mycalendarID";
DateTime startTime = DateTime.UtcNow;
DateTime endTime = startTime.AddMinutes(1); // Check for events in the next minute
// Check for events
string eventsUrl = $"https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events?timeMin={startTime:o}&timeMax={endTime:o}&singleEvents=true&orderBy=startTime";
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var eventsResponse = await httpClient.GetAsync(eventsUrl);
if (!eventsResponse.IsSuccessStatusCode)
{
return "1"; // API call failed
}
var eventsBody = await eventsResponse.Content.ReadAsStringAsync();
dynamic events = JsonConvert.DeserializeObject(eventsBody);
if (events.items.Count == 0)
{
return "2"; // No event
}
// There's an event
string eventTitle = events.items[0].summary;
return eventTitle; // Return the event title
}
}
}
static async Task Main(string[] args)
{
// Call the method to check the event
string result = await CheckEventAtCurrentTime();
// If result is a string, it's the event title; otherwise, it's an error code
if (int.TryParse(result, out int errorCode))
{
// Handle error codes
Console.WriteLine($"Error code: {errorCode}");
}
else
{
// Handle event found
Console.WriteLine($"Event: {result}");
}
}
}
edit:
This is the JWT generated by the code:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpZC1jeC1obnBAY3gtaG5wLWRlbW8uaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvY2FsZW5kYXIucmVhZG9ubHkiLCJhdWQiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImV4cCI6MTcyNjEyOTI1NywiaWF0IjoxNzI2MTI1NjU3fQ.FYAZeCZl8qbI9hE3xlTWl2DP6_rxazGzQoE9E9DHEaWVp_9HQHoN-sqZ3yX3stV17GqZ2gErE0AMkb_2SJF73ThlrxtJOFbMf2nOlOSHZJKLbKFlWtfO7Wd-4gXrMJXLtAROCrVhm7Wen5__6ga3l1ez_lV1aRWRatNbtk-zBkR0x-RWZbxZc2LMVGV_kAiXAORGWFO_unH6EQTG-n5ZckVKGjRm4MhxGx5hnMr46zchuc30N_N0bT7j1h4S4P_ignFT3dUllVlQQFNYKOPTWOm4qMozro_43PelKSjbkmhqHfgHxDw70IbBxTXcY4P8eiiT9dYzmmiqz27fKh8A0Q
And this is the public key:
edit2: found the correct key, the issue was the public key from Google Cloud that I had to retrieve via gcloud shell
6