I am trying to integrate an existing application with Google's OAuth2 system to make more secure calls to Google Services such as GMail and move away from the standard SMTP client sending techniques in C#. I have set up a service account and obtained a P12 key and everything is in place and the calls are going to the OAuth server but I keep getting the following JSON response back when the call to 'GetResponse' is made:
{"error":"invalid_grant","error_description":"Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframe. Check your iat and exp values in the JWT claim."}
Below is the C# code I am using to make this call and I also get the same results via CURL. Every time I fiddle with "iap" and "exp" values either using local or UTC times I get different messages back. It seem that the code I have now is what I should be using but I cannot determine why I'm still getting these messages. I have made sure, according to Google Documentation, that the system clock is in sync with the Google NTP of time.google.com, but still no change. I am doing something wrong with the construction of the JSON Web Token (JWT) when making the web request call?
try
{
RSACryptoServiceProvider rsaCryptoServiceProvider = new RSACryptoServiceProvider();
List<string> jwtSegments = new List<string>();
var jwtHeader = new {
alg = "RS256",
typ = "JWT"
};
int nowTimeInEpochSeconds = (int)Math.Floor((DateTime.Now - new DateTime(1970, 1, 1)).TotalSeconds);
var jwtClaimSet = new {
iss = "***********@********.iam.gserviceaccount.com",
scope = "https://www.googleapis.com/auth/gmail.send",
aud = "https://oauth2.googleapis.com/token",
exp = nowTimeInEpochSeconds 3600,
iat = nowTimeInEpochSeconds
};
string serializedHeader = JsonConvert.SerializeObject(jwtHeader, Formatting.None);
string serializedClaimSet = JsonConvert.SerializeObject(jwtClaimSet, Formatting.None);
byte[] jwtHeaderBytes = Encoding.UTF8.GetBytes(serializedHeader);
byte[] jwtClaimSetBytes = Encoding.UTF8.GetBytes(serializedClaimSet);
string base64EncodedHeader = Base64UrlEncode(jwtHeaderBytes);
string base64EncodedClaimSet = Base64UrlEncode(jwtClaimSetBytes);
jwtSegments.Add(base64EncodedHeader);
jwtSegments.Add(base64EncodedClaimSet);
string jwtRequestToSign = string.Join(".", jwtSegments);
byte[] jwtRequestBytesToSign = Encoding.UTF8.GetBytes(jwtRequestToSign);
X509Certificate2 cert = new X509Certificate2(@"C:\**************.p12", "notasecret");
AsymmetricAlgorithm rsaSignature = cert.PrivateKey;
byte[] signature = rsaCryptoServiceProvider.SignData(jwtRequestBytesToSign, "SHA256");
string jwt = jwtRequestToSign "." Base64UrlEncode(signature);
WebRequest webRequestForaccessToken = WebRequest.Create("https://oauth2.googleapis.com/token") as HttpWebRequest;
string accessTokenRequestParameters = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" jwt;
byte[] accessTokenRequestFromJwt = Encoding.UTF8.GetBytes(accessTokenRequestParameters);
string token = string.Empty;
if (webRequestForaccessToken != null)
{
webRequestForaccessToken.Method = "POST";
webRequestForaccessToken.ContentType = "application/x-www-form-urlencoded";
webRequestForaccessToken.ContentLength = accessTokenRequestFromJwt.Length;
using (Stream stream = webRequestForaccessToken.GetRequestStream())
{
stream.Write(accessTokenRequestFromJwt, 0, accessTokenRequestFromJwt.Length);
}
using (HttpWebResponse httpWebResponseForaccessToken = webRequestForaccessToken.GetResponse() as HttpWebResponse)
{
Stream streamForaccessToken = httpWebResponseForaccessToken?.GetResponseStream();
if (streamForaccessToken != null)
{
using (StreamReader streamReaderForForaccessToken = new StreamReader(streamForaccessToken))
{
string jsonUserResponseString = streamReaderForForaccessToken.ReadToEnd();
JObject groupsIoApiUserObject = JObject.Parse(jsonUserResponseString);
token = groupsIoApiUserObject["access_token"].ToString();
}
}
}
}
}
catch (WebException ex)
{
try
{
string resp = new StreamReader(ex.Response.GetResponseStream()).ReadToEnd();
Console.WriteLine(resp);
}
catch (Exception parseException)
{
string m = parseException.Message;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
CodePudding user response:
two reasons for that either your method of calculation is not good, or the time of your server is not correct, can check with something like:
int nowTimeInEpochSeconds = DateTimeOffset.UtcNow.AddMinutes(59).ToUnixTimeSeconds()
CodePudding user response:
So as it turns out there were two things that needed correction. The first was that according the answer provided by @GAOUL, I was not calculating the epoch time correctly (though I had to modify the suggested line to remove the "AddMinutes" method to get the correct value for the "iat" field). Then, the "exp" value became the "iat" value plus 3600 (1 hour).
So my original time calculation went from this:
int nowTimeInEpochSeconds = (int)Math.Floor((DateTime.Now - new DateTime(1970, 1, 1)).TotalSeconds);
became this:
int nowTimeInEpochSeconds = (int)Math.Floor((double)DateTimeOffset.UtcNow.ToUnixTimeSeconds());
The second issue I discovered after reviewing my own code was that I not correctly signing the signature with the private key (was not signing with private key at all).
So I replaced this branch of code:
X509Certificate2 cert = new X509Certificate2(@"C:\**************.p12", "notasecret");
AsymmetricAlgorithm rsaSignature = cert.PrivateKey;
byte[] signature = rsaCryptoServiceProvider.SignData(jwtRequestBytesToSign, "SHA256");
with this branch instead:
X509Certificate2 certificate = new X509Certificate2(@"**************.p12", "notasecret");
RSA rsa = certificate.GetRSAPrivateKey();
byte[] signature = rsa.SignData(jwtRequestBytesToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
Once I applied these changes, I got a valid response back from the Google OAuth server and it contained the access/refresh tokens along with the other fields.
Also, thanks to @dazwilkin for referencing JWT.io. That was also a big help!