2

I read the tutorial in https://developers.google.com/accounts/docs/OAuth2ServiceAccount

and tried using their example but keep getting 400 bad request. this is my code:

 ClaimSet cs = new ClaimSet()
        {
            aud = "https://www.googleapis.com/oauth2/v3/token",
            iss = "1070248278615-hoq0meaunarl9hj8t9klg4gqkohlme9u@developer.gserviceaccount.com",
            exp = GetTime(DateTime.UtcNow.AddHours(1)).ToString(),
            iat = GetTime(DateTime.UtcNow).ToString(),
            scope = "https://www.googleapis.com/auth/freebase"
        };

        //get the signed JWT
        var signedJwt = JsonWebToken.Encode(cs);           


public static string Encode(object payload, JwtHashAlgorithm algorithm = JwtHashAlgorithm.RS256)
        {
            return Encode(payload, Encoding.UTF8.GetBytes(PrivateKey), algorithm);
        }

    public static string Encode(object payload, byte[] keyBytes, JwtHashAlgorithm algorithm)
    {
        var segments = new List<string>();
        var header = new { alg = algorithm.ToString(), typ = "JWT" };

        byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
        byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));

        segments.Add(Base64UrlEncode(headerBytes));
        segments.Add(Base64UrlEncode(payloadBytes));

        var stringToSign = string.Join(".", segments);

        var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);

        byte[] signature = HashAlgorithms[algorithm](keyBytes, bytesToSign);
        segments.Add(Base64UrlEncode(signature));

        return string.Join(".", segments.ToArray());
    }
        using (var wb = new WebClient())
        {
            var url = "https://www.googleapis.com/oauth2/v3/token/";
            wb.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
            var data2 = new NameValueCollection();
            data2["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
            data2["assertion"] = signedJwt;
            var response2 = wb.UploadValues(url, "POST", data2);
        }

Now after getting the access token I try to write to freebase: using the following tutorial I saw that I should Get verb: https://developers.google.com/accounts/docs/OAuth2ServiceAccount#creatinganaccount

    var url = "https://www.googleapis.com/freebase/v1/mqlwrite";
                wb.QueryString.Add("lang", "/lang/en");
                wb.QueryString.Add("query", "%5B%7B%0A%20%20%22mid%22%3A%20%22%2Fm%2F011840dm%22%2C%0A%20%20%22%2Fcommon%2Ftopic%2Ftopic_equivalent_webpage%22%3A%20%7B%0A%20%20%20%20%22connect%22%3A%20%22insert%22%2C%0A%20%20%20%20%22value%22%3A%20%22http%3A%2F%2Fwww.imdb.com%2Fname%2Fnm4963898%2F%22%0A%20%20%7D%0A%7D%5D");
                wb.Headers.Add("Authorization", "Bearer " + accesstoken);
                var ResponseBytes = wb.DownloadString(url);

Appreciate the help :)

1 Answers1

2

You're including the Content-Type in the POST data but it should be presented as part of the HTTP headers as in:

wb.Headers.Add("Content-Type","application/x-www-form-urlencoded");

However, using UploadValues this will be the default setting anyway unless overridden.

Other than that, UploadValues will automatically URL-encode your values, so you should present them in their raw form; so for the grant type that would mean:

data2["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";

Edit1:
Also, your JWT uses the wrong aud claim since it is set to https://accounts.google.com/o/oauth2/token instead of https://www.googleapis.com/oauth2/v3/token and is expired since July 2nd 2012.

Edit2: You must also post to the URL without the trailing slash and get the iat and exp timestamps correct. Successfully tested code using Newtonsoft.Json:

public class GoogleServiceAccountBearerJWTSample
{
    private static string Base64UrlEncode(byte[] input)
    {
        var output = Convert.ToBase64String(input);
        output = output.Split('=')[0]; // Remove any trailing '='s
        output = output.Replace('+', '-'); // 62nd char of encoding
        output = output.Replace('/', '_'); // 63rd char of encoding
        return output;
    }

    public static string Encode(object payload, AsymmetricAlgorithm rsa) {
        var segments = new List<string>();
        var header = new { alg = "RS256", typ = "JWT" };
        byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
        byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
        segments.Add(Base64UrlEncode(headerBytes));
        segments.Add(Base64UrlEncode(payloadBytes));
        var stringToSign = string.Join(".", segments.ToArray());
        var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);

        // VARIANT A - should work on non-SHA256 enabled systems
        var rs = rsa as RSACryptoServiceProvider;
        var cspParam = new CspParameters
        {
            KeyContainerName = rs.CspKeyContainerInfo.KeyContainerName,
            KeyNumber = rs.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
        };
        var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
        var signature = aescsp.SignData(bytesToSign, "SHA256");
        // END OF VARIANT A

        // VARIANT B - works on FIPS SHA256 enabled systems
        // var pkcs1 = new RSAPKCS1SignatureFormatter(rsa);
        // pkcs1.SetHashAlgorithm("SHA256");
        // var signature = pkcs1.CreateSignature(new SHA256Managed().ComputeHash(bytesToSign));
        // END OF VARIANT B

        segments.Add(Base64UrlEncode(signature));
        return string.Join(".", segments.ToArray());
   }

   public static void Main()
   {    
        var utc0 = new DateTime(1970,1,1,0,0,0,0, DateTimeKind.Utc);
        var issueTime = DateTime.UtcNow;

        var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
        var exp = (int)issueTime.AddMinutes(55).Subtract(utc0).TotalSeconds; // Expiration time is up to 1 hour, but lets play on safe side

        var payload = new {
            iss = "xxxxxxxxxxxxxxxxxx@developer.gserviceaccount.com",
            aud = "https://www.googleapis.com/oauth2/v3/token",
            scope = "https://www.googleapis.com/auth/freebase",
            exp = exp,
            iat = iat
        };

        var certificate = new X509Certificate2("google-client.p12", "notasecret");
        var signedJwt = Encode(payload, certificate.PrivateKey);

        //System.Console.WriteLine(signedJwt);

        using (var wb = new WebClient())
        {
            var url = "https://www.googleapis.com/oauth2/v3/token";
            var data2 = new NameValueCollection();
            data2["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
            data2["assertion"] = signedJwt;
            var response2 = wb.UploadValues(url, "POST", data2);
            System.Console.WriteLine(Encoding.UTF8.GetString(response2));
        }
    }
}
Hans Z.
  • 50,496
  • 12
  • 102
  • 115
  • Thank you for your response. I changed my code as you suggested (as you can see above) but unfortunately still getting the same '400 Bad Request' error. What am I doing wrong? – user2051871 Dec 30 '14 at 14:20
  • @Hans Z. Are you sure? the JWT is taken from the example in : [link]https://developers.google.com/accounts/docs/OAuth2ServiceAccount and I see they do use [link] https://www.googleapis.com/oauth2/v3/token Do you see otherwise? – user2051871 Dec 30 '14 at 15:25
  • OK, here's then the root cause of your problem: you can't take **a** JWT (like the one from Google's example) and present it... You need to generate your **own** client and associated private key in the Google API console and use that information (e-mail + private key) to create and sign **your own** JWT. The process of creating your own client is pretty well described in https://developers.google.com/accounts/docs/OAuth2ServiceAccount#creatinganaccount. @mashtagidi: you may create a separate question for your issue – Hans Z. Dec 30 '14 at 15:30
  • @HansZ. I generated my own client and associated private key in the Google API console and used the information (e-mail + private key) to create and sign my own JWT. But keep getting the same error '400 Bad Request'. I've added my code in my question. – user2051871 Dec 30 '14 at 17:01
  • I have to agree because I tried with my credentials as well and kept getting the same error. Is there a bug with the API perhaps? @Hans Z. maybe I should create another question.. – mashta gidi Dec 30 '14 at 17:05
  • Looking at your updated code, I don't see where you sign the JWT, you just encode it in its serialized form. Where do you provide the private key and the signing algorithm to use (RS256)? – Hans Z. Dec 30 '14 at 17:07
  • Can you paste in the public key and the resulting JWT so I can check its signature + contents? I'm assuming you use the code from http://stackoverflow.com/questions/10055158/is-there-a-json-web-token-jwt-example-in-c to get the private key etc. – Hans Z. Dec 30 '14 at 20:32
  • @HansZ. Actually to get the private key I pressed the 'Generate new JSON Key' button at the Developers Console under 'Service Account'. – user2051871 Dec 30 '14 at 21:04
  • @HansZ. tried to run your cod but got an erro of 'Invalid algorithm specified' at the _italic_ **bold** `var signature = pkcs1.CreateSignature(new SHA256Managed().ComputeHash(bytesToSign))` – user2051871 Dec 31 '14 at 11:37
  • @HansZ. Tried replacing the 'signautre producing' lines of code (mentioned in the above comment) with this two lines : _italic_ **bold** `var pkcs1 = new HMACSHA256(Encoding.UTF8.GetBytes(certificate.PrivateKey.ToString())); var signature = pkcs1.ComputeHash(bytesToSign);` but got again the 400 Bad Request error :( – user2051871 Dec 31 '14 at 11:51
  • don't change back to HMAC, that's just wrong; wrt. to "invalid algorithm" see http://blogs.msdn.com/b/alejacma/archive/2010/05/25/invalid-algorithm-specified-when-signing-with-rsacryptoserviceprovider-and-sha-256.aspx – Hans Z. Dec 31 '14 at 12:05
  • @HansZ. I apologize, I use Win7 and I don't understand why microsoft rsaenh.dll doesn't support my operation nor do I understand how to solve the issue... – user2051871 Dec 31 '14 at 14:35
  • try the edited code, i.e Variant A instead of the earlier, now commented out Variant B; A seems to work for non-FIPS windows systems though I've not actually confirmed this myself (copied from some code that's floating around); even if it works, for all of this it is probably better to switch to: https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/ – Hans Z. Dec 31 '14 at 21:05
  • @HansZ. Thank you it worked and I manage to get the access token. I tried using it with my request (using web client) and passing the Accesstoken in the header (Edited code will show above) and got '400 bad request' – user2051871 Jan 04 '15 at 12:50
  • Perhaps the parameters need to be POSTED instead of passing them as query params? – Hans Z. Jan 04 '15 at 13:17
  • @HansZ. I tried sending Post but kept getting 404 from the server. In the documentations I see that it says to use Get method. I was expecting it to be Post indeed. – user2051871 Jan 04 '15 at 15:12
  • check the HTML content that is returned by Google; trying your example gives me: `{ "error": { "errors": [ { "domain": "global", "reason": "invalid", "message": "query is invalid: JSON parsing error.", "locationType": "other", "location": "parameters.query" } ], "code": 400, "message": "query is invalid: JSON parsing error." } } ` – Hans Z. Jan 04 '15 at 17:13
  • @HansZ. I created the query again and encoded it. This time got 403 Error code... :( – user2051871 Jan 05 '15 at 14:35
  • could be that your access token has expired (happened to me earlier) and you may need to generate a fresh one – Hans Z. Jan 05 '15 at 15:00
  • The 403 may be a sign that it actually works... when I try this I also get the 403 and the HTML in the response says: `"A Freebase account is required to contribute, please go to http://freebase.com to register."`; I don't have that – Hans Z. Jan 05 '15 at 15:37
  • @HansZ. might it be that freebase doesn't support service account authentication? [link](https://groups.google.com/forum/#!topic/freebase-discuss/9Y5COjIGgBc) – user2051871 Jan 05 '15 at 16:42
  • @HansZ. How do you think I should handle it from here? – user2051871 Jan 05 '15 at 17:42
  • as suggested in the thread: use the Authorization Code flow to obtain a refresh token from a user that you've logged in via Google – Hans Z. Jan 05 '15 at 17:58
  • @HansZ. what I don't understand is how to do so without a user consent page, which I don't need obviously... this is available in the service account which is not the correct way apparently :/ – user2051871 Jan 06 '15 at 10:39
  • Go through consent once while supplying the `access_type=offline` parameter in the request to the authorization endpoint and use the refresh token that is returned from the token endpoint to get new access token when the old one expires; no need to go through user consent anymore after that bootstrapping phase – Hans Z. Jan 06 '15 at 10:44
  • @HansZ. Do you think I should use the following tutorial? [Using OAuth 2.0 for Installed Applications] (https://developers.google.com/accounts/docs/OAuth2InstalledApp) – user2051871 Jan 06 '15 at 11:20
  • yes, or OAuth 2.0 for Web Server apps, depending on what type of app your building – Hans Z. Jan 06 '15 at 11:22
  • right, using the code flow you'll have a bootstrapping challenge firing up the browser and getting back the `code` in to the app, but since it is about yourself, you can probably copy/paste it from the browser URL on return – Hans Z. Jan 06 '15 at 11:45
  • @HansZ. I don't mine doing it through asp.net if needed. What is the best way you would think doing so? and which way do you think I should take? (OAuth 2.0 for Web Server apps or OAuth 2.0 for Installed Applications) – user2051871 Jan 07 '15 at 09:00