1

I'm using an environment that doesn't have native support for a GCP client library. So I'm trying to figure out how to authenticate directly using manually crafted JWT token.

I've adapted the tasks from here Using nodeJS test environment, with jwa to implement the algorithm.

https://developers.google.com/identity/protocols/OAuth2ServiceAccount

The private key is taken from a JSON version of the service account file.

When the test runs, it catches a very basic 400 error, that just says "invalid request". I'm not sure how to troubleshoot it.

Could someone please help identify what I'm doing wrong?

var assert = require('assert');
const jwa = require('jwa');
const request = require('request-promise');

const pk = require('../auth/tradestate-2-tw').private_key;

const authEndpoint = 'https://www.googleapis.com/oauth2/v4/token';


describe('Connecting to Google API', function() {

    it('should be able to get an auth token for Google Access', async () => {
      assert(pk && pk.length, 'PK exists');
      const header = { alg: "RS256", typ: "JWT" };
      const body = {
        "iss":"salesforce-treasury-wine@tradestate-2.iam.gserviceaccount.com",
        "scope":"https://www.googleapis.com/auth/devstorage.readonly",
        "aud":"https://www.googleapis.com/oauth2/v4/token",
        "exp": new Date().getTime() + 3600 * 1000,
        "iat": new Date().getTime()
      };
      console.log(JSON.stringify(body, null, 2));
      const encodedHeader = Buffer.from(JSON.toString(header)).toString('base64')
      const encodedBody = Buffer.from(JSON.toString(body)).toString('base64');
      const cryptoString = `${encodedHeader}.${encodedBody}`;
      const algo = jwa('RS256');
      const signature = algo.sign(cryptoString, pk);
      const jwt = `${encodedHeader}.${encodedBody}.${signature}`;
      console.log('jwt', jwt);
      const headers = {'Content-Type': 'application/x-www-form-urlencoded'};
      const form = {
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        assertion: jwt
      };
      try { 
        const result = await request.post({url: authEndpoint, form, headers});
        assert(result, 'Reached result');
        console.log('Got result', JSON.stringify(result, null, 2));
      } catch (err) {
        console.log(JSON.stringify(err, null, 2));
        throw (err);
      }

    });
});
Richard G
  • 5,243
  • 11
  • 53
  • 95
  • I wrote an article that details the steps involved. This includes real source code in Python to show you the steps. The important major steps, how to create the JWT and how to exchange the JWT for tokens are explained. https://www.jhanley.com/google-cloud-creating-oauth-access-tokens-for-rest-api-calls/ – John Hanley May 26 '19 at 05:24
  • One other thing I'd do is for iat and exp use the same new Date() object instead of calling that twice. – abelito May 26 '19 at 05:56
  • Hi @abelito I've tried that, but it's causing error as per your answer below. But the change to JSON.stringify works well. But now I have another issue with the iad and expiry time... Could it be timezone? – Richard G May 26 '19 at 06:04

1 Answers1

1

Use JSON.stringify instead of JSON.toString. From the link in your question:

{"alg":"RS256","typ":"JWT"}

The Base64url representation of this is as follows:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9

When using JSON.toString() and then base64 encoding, you would get W29iamVjdCBKU09OXQ== which explains the 400 for an invalid request as it can't decrypt anything you're sending it.

Community
  • 1
  • 1
abelito
  • 1,094
  • 1
  • 7
  • 18
  • Oh great, that's a relief. I was going cross eyed trying to figure out what was wrong. I've got a step further to a new error: ' StatusCodeError: 400 - "{\n \"error\": \"invalid_grant\",\n \"error_description\": \"Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframe. Check your iat and exp values and use a clock with skew to account for clock differences between systems.\"\n}"' – Richard G May 26 '19 at 06:00
  • This is the code I changed as per a previous comment: const now = new Date().getTime(); const then = now + 3600 * 1000; const body = { "iss":"salesforce-treasury-wine@tradestate-2.iam.gserviceaccount.com", "scope":"https://www.googleapis.com/auth/devstorage.readonly", "aud":"https://www.googleapis.com/oauth2/v4/token", "exp": then, "iat": now }; – Richard G May 26 '19 at 06:00
  • I'm glad the stringify helped! I'm thinking about your new issue, but for now try a shorter time period.. maybe 15 or 30 minutes? Could maybe be a UTC vs local timezone issue like you mentioned in your above comment – abelito May 26 '19 at 06:17
  • @RichardG https://stackoverflow.com/questions/36189612/token-must-be-a-short-lived-token-and-in-a-reasonable-timeframe Looks like potentially the computer/server you're making the request from has a system clock that's out of sync with NTP – abelito May 26 '19 at 06:29
  • I don't think it could be system clock. I'm on my local Mac and it's set to get the time automatically, clock seems okay. I've tried shortening the time stamp as well, but the same issue. Perhaps an encoding issue? Is there some way to force the string to UTF-8? – Richard G May 26 '19 at 09:09
  • 1
    Ah got it at last! The date in iat and exp are in "seconds" not in milliseconds. So I needed to convert it: `const now = Date().getTime() / 1000`. Thanks for the help I'll mark this correct now. – Richard G May 26 '19 at 10:05