28

I'm trying to set up an analytics dashboard of my site for my leadership to view site usage. I don't want them to have to have a google account or to add them individual to see the results.

I've set up a service account and OAuth2 access. All the tutorials I find show code like this:

gapi.analytics.auth.authorize({
  clientid: 'Service account client ID',
  serverAuth: {
      access_token: 'XXXXXXXXXXXXXXXXX'
}

And all the documentation talks about "...once you recieve your access token...." But none of them actually say how to get that! I see Certificate Fingerprints, Public key fingerprints. I also see how to generate JSON and P12 keys. I don't see how to generate the access token.

Can someone explain how to do this?

I found this. It explains that I need the key file and that it is a bad idea, but doesn't say how to actually do it.

I also found this. But I don't know anything about Node.js and I'm hoping that is just one possible route?

Community
  • 1
  • 1
Rothrock
  • 1,413
  • 2
  • 16
  • 39
  • I found this javascript library http://kjur.github.io/jsjws/index.html#demo anybody have experience with it? – Rothrock Mar 03 '15 at 16:53
  • Where do I get my private key? I've downloaded the .p12 file, but it isn't readable by me. I used an online service to convert the .p12 to a PEM file which I can open in notepad, but the private key shown there is too short compared to others I've seen. – Rothrock Mar 04 '15 at 18:38

5 Answers5

34

Finally got it working! Using kjur's jsjws pure JavaScript implementation of JWT. I used this demo as the basis for generating the JWT to request the token. Here are the steps

In the Google Developers console I created a service account. Here are instructions for that

In the Google API console I added the service account to the credentials. I then generated a new JSON key. This gives me my private key in plain text format.

I then followed these instructions from google on making an authorized API call using HTTP/REST.

This is the required header information.

var pHeader = {"alg":"RS256","typ":"JWT"}
var sHeader = JSON.stringify(pHeader);

And the claim set is something like this. (This is using syntax that is supplied by the KJUR JWT library described above.)

var pClaim = {};
pClaim.aud = "https://www.googleapis.com/oauth2/v3/token";
pClaim.scope = "https://www.googleapis.com/auth/analytics.readonly";
pClaim.iss = "<serviceAccountEmail@developer.gserviceaccount.com";
pClaim.exp = KJUR.jws.IntDate.get("now + 1hour");
pClaim.iat = KJUR.jws.IntDate.get("now");

var sClaim = JSON.stringify(pClaim);

The the controversial bit is putting my private key into the client side code. For this usage it isn't that bad (I don't think.) First, the site is behind our corporate firewall, so who is going to "hack" it? Second, even if someone did get it, the service account's only authorization is to view our analytics data -- which is the purpose of my dashboard is that anybody who visits the page can view our analytics data. Not going to post the private key here, but basically like so.

var key = "-----BEGIN PRIVATE KEY-----\nMIIC....\n-----END PRIVATE KEY-----\n";`enter code here`

Then generated a signed JWT with

 var sJWS = KJUR.jws.JWS.sign(null, sHeader, sClaim, key);

After that I used XMLHttpRequest to call the google API. I tried to use FormData with the request but that didn't work. So the old(er) school

var XHR = new XMLHttpRequest();
var urlEncodedData = "";
var urlEncodedDataPairs = [];

urlEncodedDataPairs.push(encodeURIComponent("grant_type") + '=' + encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer"));
urlEncodedDataPairs.push(encodeURIComponent("assertion") + '=' + encodeURIComponent(sJWS));
urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');

// We define what will happen if the data are successfully sent
XHR.addEventListener('load', function(event) {
    var response = JSON.parse(XHR.responseText);
    token = response["access_token"]
});

// We define what will happen in case of error
XHR.addEventListener('error', function(event) {
    console.log('Oops! Something went wrong.');
});

XHR.open('POST', 'https://www.googleapis.com/oauth2/v3/token');
XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
XHR.send(urlEncodedData)

After that I have my access token and I can follow these tutorials on using the embed API, but authorizing like so:

gapi.analytics.auth.authorize({
    serverAuth: {
        access_token: token
    }
});

Don't forget that you have to give the service account permission to view the content, just like any other user. And of course, it would be a really bad idea if the service account was authorized to do anything other than read only.

There are probably also issues with regards to timing and token expiring that I will run into, but so far so good.

Rothrock
  • 1,413
  • 2
  • 16
  • 39
  • Since I don't have 10 rep, I can't post more than 2 links. So if anybody ever needs to know where I found the various tutorials please ask and I can share. – Rothrock Mar 26 '15 at 20:18
  • have you got some tutorial for service account auth2 login? – Praveen Dabral Jun 08 '15 at 06:21
  • I followed this here https://developers.google.com/identity/protocols/OAuth2ServiceAccount but you've probably already seen that. What specifically are you looking for? – Rothrock Jun 09 '15 at 21:20
  • Actually i was looking to implement oauth2 login in zend library but now i've done it through google client library. Thanks for your time. – Praveen Dabral Jun 10 '15 at 05:19
  • Hi Rothrock, i facing the same issue. I created the service account and its give me json file and to extract access token from json file?. Can you please help me. – Sivabalan Dec 24 '15 at 04:54
  • 1
    It has been awhile. But I just tried it again. In the developer's console create a new service account. At that time it gives you the ability to download the json key. Open the file with text edit and the key is the value of the "private key" property. It begins -----BEGIN PRIVATE KEY----- and ends with -----END PRIVATE KEY-----\n Remember that the private key should be kept secret and not exposed to end users or included in a javascript file that will be delivered to the browser. All the crypto stuff is supposed to happen on the server php, aspx, nodejs, etc.... – Rothrock Dec 28 '15 at 17:43
  • 4
    You have no idea how long I have been looking for this. I've spent so much time scouring the web for a full-fledged, step-by-step example (Google's documentation is s***!). Thank you so much for this. – CaffeinatedMike Jan 30 '17 at 16:45
  • Stupid Question, but do you keep the -----BEGIN PRIVATE KEY-----\n part of the private key? – CaffeinatedMike Jan 31 '17 at 18:58
  • Yes you keep that in there. The whole thing. Remember that you shouldn't store the private key in any place that a clever person with debug tools could read it. – Rothrock Jan 31 '17 at 19:33
  • 1
    Thanks for the answer @Rothrock. I'm now getting stuck with the error `"errorDetail": "JWS Head is not safe JSON string: {'alg':'RS256','typ':'JWT'} (jsjws/jws3.2.js#407)", "statusCode": "500"`. Any idea why? I'm trying to create a script on [this site](scriptr.io), so my project is headless (no window element). – CaffeinatedMike Feb 01 '17 at 17:01
  • Sorry for the delay. I would recommend starting a new thread. Wind windowless scriptr.io you are out of my league and there are probably others who could help more with that. – Rothrock Feb 06 '17 at 16:01
  • hey Rothrock, I also tried your given code but I didn't get access token. After gettiing access token how to get google analytics data? – Menu Feb 24 '17 at 14:23
  • First you'll have to get the token working! I would recommend starting a new thread on that. After that there is a link in my first post to tutorials on Embed API. Remember that this technique exposes your secret to anybody with a debugger (everyone!) and I only recommend it inside a corporate intranet or other situation where you can rely on a bit of security from other situations. – Rothrock Feb 24 '17 at 15:42
  • If I make the service account an owner just for testing shouldn't that suffice for getting a token? – N-ate Nov 09 '17 at 23:27
  • you're a real genius, don't know how you found how to urlencode the data, documentation is a bit labyrinthic and sometimes pretty vague! If i could i'd upvote 10 times – Kaddath Nov 16 '17 at 15:57
  • 1
    As of 01/2018, I suggest using https://github.com/kjur/jsrsasign instead of jsjws - it contains jsjws internally and fixes the problem @CaffeinatedCoder had. It's just a drop-in replacement, function calls are exactly the same. Btw thank you for this answer, I used it while testing the new FCM HTTP v1 API. – Arx Jan 09 '18 at 16:14
  • The above answer did put me on the right track but the actual google API documentation ended up being a lot more useful for me: https://developers.google.com/identity/protocols/oauth2/service-account#httprest (it took me a LONG time to find it) – mathieu May 26 '21 at 15:50
  • Following OP's example, HTTP 400 on XHR to "...oauth2/v3/token". Fixed it by removing the leading "<" in value for pClaim.iss – ZachHappel Aug 04 '21 at 17:54
  • instead of `access_token`, I am getting `id_token` in the response. Any idea how to get `access_token` – Shrinivas Shukla Sep 17 '22 at 22:30
14

You can use the official (and alpha) Google API for Node.js to generate the token. It's helpful if you have a service account.

On the server:

npm install -S googleapis

ES6:

import google from 'googleapis'
import googleServiceAccountKey from '/path/to/private/google-service-account-private-key.json' // see docs on how to generate a service account

const googleJWTClient = new google.Auth.JWT(
  googleServiceAccountKey.client_email,
  null,
  googleServiceAccountKey.private_key,
  ['https://www.googleapis.com/auth/analytics.readonly'], // You may need to specify scopes other than analytics
  null,
)

googleJWTClient.authorize((error, access_token) => {
   if (error) {
      return console.error("Couldn't get access token", e)
   }
   // ... access_token ready to use to fetch data and return to client
   // even serve access_token back to client for use in `gapi.analytics.auth.authorize`
})
ajbeaven
  • 9,265
  • 13
  • 76
  • 121
hwrod
  • 461
  • 4
  • 6
5

Now, the Service Account authentication has a getAccessToken method that do that for us.

const {google} = require('googleapis');

const main = async function() {
    
    const auth = new google.auth.GoogleAuth({
        keyFile: __dirname  + '/service-account-key.json',
        scopes: [ 'https://www.googleapis.com/auth/cloud-platform']
    });

    const accessToken = await auth.getAccessToken()
    
    console.log(JSON.stringify(auth, null, 4))
    console.log(JSON.stringify(accessToken, null, 4));
}

main().then().catch(err => console.log(err));

Anderson Marques
  • 808
  • 8
  • 13
  • I am following your logic, I am getting below error : FetchError: request to https://www.googleapis.com/oauth2/v4/token failed, reason: getaddrinfo ENOTFOUND 808 – Ravi Sharma Jul 15 '21 at 12:38
  • I used this way. But got access token with this type "abcxyz......", all the last character is "dot" and cannot be used. Do you know why? – VitDuck Aug 25 '21 at 09:46
2

I came across this question while looking for something similar and thought I'd share a node.js solution I ended up with. In essence, I saved a google service account to a sa.json file and then used it to sign a jwt I sent to gcp.

const jwt = require("jsonwebtoken");
const sa = require("./sa.json");
const fetch = require("isomorphic-fetch");

const authUrl = "https://www.googleapis.com/oauth2/v4/token";
const scope = "https://www.googleapis.com/auth/cloud-platform";

const getSignedJwt = () => {
  const token = {
    iss: sa.client_email,
    iat: parseInt(Date.now() / 1000),
    exp: parseInt(Date.now() / 1000) + 60 * 60, // 60 minutes
    aud: authUrl,
    scope,
  };

  return jwt.sign(token, sa.private_key, { algorithm: "RS256" });
};

const getGoogleAccessToken = async () => {
  const signedJwt = getSignedJwt();
  const body = new URLSearchParams();

  body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
  body.append("assertion", signedJwt);

  const response = await fetch(authUrl, {
    method: "post",
    headers: {
      Authorization: `Bearer ${signedJwt}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body,
  });
  return response.json();
};

(async () => {
  const tokenResp = await getGoogleAccessToken();
  console.log(tokenResp);
})();

Troy
  • 1,799
  • 3
  • 20
  • 29
-2

You have (below) without single quotes

gapi.analytics.auth.authorize({
    serverAuth: {
        access_token: token
    }
});

but for it to work, as per their documentation you need to put single quotes around serverAuth and access_token.

gapi.analytics.auth.authorize({
    'serverAuth': {
        'access_token': token
    }
});
RichardBernards
  • 3,146
  • 1
  • 22
  • 30
user3051556
  • 15
  • 1
  • 2
  • 1
    This is incorrect. The argument passed to authorize(...) is a JavaScript Object defined using a JavaScript Object Initializer. This allows for properties to be defined without quotes, unlike JSON https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Object_initializer#Object_literal_notation_vs_JSON. – thisismyrobot Oct 16 '16 at 02:17