Search code examples
c#oauth-2.0google-apigoogle-oauthjwt

Google OAuth2 with Server to Server authentication returns "invalid_grant"


I am trying to follow the steps outlined here in order to acquire the access token for use with the Google Calendar API with OAuth2. After attempting to put together and sign the jwt, I always end up with a 400 "Bad request" response, with the error "invalid_grant".

I have followed the steps quite closely and have meticulously checked each line multiple times. I have also exhaustively looked through each and every post on the topic that I could find. I am so thoroughly stumped that I have written my first ever SO question after many years of finding solutions online.

Commonly proposed solutions that I have already tried:

1) My system clock is sync'd with ntp time

2) I am using the email for iss, NOT the client ID.

3) My issued times and expiration times are in UTC

4) I did look into the access_type=offline parameter, but it does not seem to apply in this server-to-server scenario.

5) I did not specify the prn parameter.

6) Various other misc things

I am aware that there are Google libraries that help manage this, but I have reasons for why I need to get this working by signing the jwt myself without using the provided libraries. Also, many of the questions and samples I've seen thus far seem to be using accounts.google.com/o/oauth2/auth for the base url, whereas the documentation I linked above seems to specify that the request go to www.googleapis.com/oauth2/v3/token instead (so it seems that many of the existing questions might apply to a different scenario). In any case, I'm totally stumped and don't know what else to try. Here is my C# code, with a few specific strings redacted.

    public static string GetBase64UrlEncoded(byte[] input)
    {
        string value = Convert.ToBase64String(input);
        value = value.Replace("=", string.Empty).Replace('+', '-').Replace('/', '_');
        return value;
    }

    static void Main(string[] args)
    {                       
        DateTime baseTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        DateTime now = DateTime.Now.ToUniversalTime();
        int ticksIat = ((int)now.Subtract(baseTime).TotalSeconds);
        int ticksExp = ((int)now.AddMinutes(55).Subtract(baseTime).TotalSeconds);
        string jwtHeader = @"{""typ"":""JWT"", ""alg"":""RS256""}";
        string jwtClaimSet = string.Format(@"{{""iss"":""************-********************************@developer.gserviceaccount.com""," +
                                           @"""scope"":""https://www.googleapis.com/auth/calendar.readonly""," +
                                           @"""aud"":""https://www.googleapis.com/oauth2/v3/token"",""exp"":{0},""iat"":{1}}}", ticksExp, ticksIat);                        
        byte[] headerBytes = Encoding.UTF8.GetBytes(jwtHeader);
        string base64jwtHeader = GetBase64UrlEncoded(headerBytes);
        byte[] claimSetBytes = Encoding.UTF8.GetBytes(jwtClaimSet);
        string base64jwtClaimSet = GetBase64UrlEncoded(claimSetBytes);            
        string signingInputString = base64jwtHeader + "." + base64jwtClaimSet;
        byte[] signingInputBytes = Encoding.UTF8.GetBytes(signingInputString);
        X509Certificate2 pkCert = new X509Certificate2("<path to cert>.p12", "notasecret");                                                           
        RSACryptoServiceProvider  rsa = (RSACryptoServiceProvider)pkCert.PrivateKey;
        CspParameters cspParam = new CspParameters
        {
            KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
            KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
        };

        RSACryptoServiceProvider cryptoServiceProvider = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };                
        byte[] signatureBytes = cryptoServiceProvider.SignData(signingInputBytes, "SHA256");
        string signatureString = GetBase64UrlEncoded(signatureBytes);            
        string finalJwt = signingInputString + "." + signatureString;

        HttpClient client = new HttpClient();
        string url = "https://www.googleapis.com/oauth2/v3/token?grant_type=urn%3aietf%3aparams%3aoauth%3agrant-type%3ajwt-bearer&assertion=" + finalJwt;
        HttpResponseMessage message = client.PostAsync(url, new StringContent(string.Empty)).Result;
        string result = message.Content.ReadAsStringAsync().Result;
    }

This is using a google "Service Account" which I set up on my google account, with a generated private key and its corresponding .p12 file used directly.

Has anyone gotten this approach to work? I would strongly appreciate any help at all!


Solution

  • You're POSTing to the token endpoint but the parameters are sent as part of the query string. You should send the parameters as URL form-encoded values in the POST body. For example:

    var params = new List<KeyValuePair<string, string>>();
    params.Add(new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"));
    params.Add(new KeyValuePair<string, string>("assertion", finalJwt));
    var content = new FormUrlEncodedContent(pairs);
    var message = client.PostAsync(url, content).Result;