-
-
Save dinvlad/425a072c8d23c1895e9d345b67909af0 to your computer and use it in GitHub Desktop.
/* This script auto-generates a Google OAuth token from a Service Account key, | |
* and stores that token in accessToken variable in Postman. | |
* | |
* Prior to invoking it, please paste the contents of the key JSON | |
* into serviceAccountKey variable in a Postman environment. | |
* | |
* Then, paste the script into the "Pre-request Script" section | |
* of a Postman request or collection. | |
* | |
* The script will cache and reuse the token until it's within | |
* a margin of expiration defined in EXPIRES_MARGIN. | |
* | |
* Thanks to: | |
* https://paw.cloud/docs/examples/google-service-apis | |
* https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests | |
* https://gist.github.com/madebysid/b57985b0649d3407a7aa9de1bd327990 | |
* https://github.com/postmanlabs/postman-app-support/issues/1607#issuecomment-401611119 | |
*/ | |
const ENV_SERVICE_ACCOUNT_KEY = 'serviceAccountKey'; | |
const ENV_JS_RSA_SIGN = 'jsrsasign'; | |
const ENV_TOKEN_EXPIRES_AT = 'tokenExpiresAt'; | |
const ENV_ACCESS_TOKEN = 'accessToken'; | |
const JS_RSA_SIGN_SRC = 'https://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js'; | |
const GOOGLE_OAUTH = 'https://www.googleapis.com/oauth2/v4/token'; | |
// add/remove your own scopes as needed | |
const SCOPES = [ | |
'https://www.googleapis.com/auth/userinfo.email', | |
'https://www.googleapis.com/auth/userinfo.profile', | |
]; | |
const EXPIRES_MARGIN = 300; // seconds before expiration | |
const getEnv = name => | |
pm.environment.get(name); | |
const setEnv = (name, value) => | |
pm.environment.set(name, value); | |
const getJWS = callback => { | |
// workaround for compatibility with jsrsasign | |
const navigator = {}; | |
const window = {}; | |
let jsrsasign = getEnv(ENV_JS_RSA_SIGN); | |
if (jsrsasign) { | |
eval(jsrsasign); | |
return callback(null, KJUR.jws.JWS); | |
} | |
pm.sendRequest(JS_RSA_SIGN_SRC, (err, res) => { | |
if (err) return callback(err); | |
jsrsasign = res.text(); | |
setEnv(ENV_JS_RSA_SIGN, jsrsasign); | |
eval(jsrsasign); | |
callback(null, KJUR.jws.JWS); | |
}); | |
}; | |
const getJwt = ({ client_email, private_key }, iat, callback) => { | |
getJWS((err, JWS) => { | |
if (err) return callback(err); | |
const header = { | |
typ: 'JWT', | |
alg: 'RS256', | |
}; | |
const exp = iat + 3600; | |
const payload = { | |
aud: GOOGLE_OAUTH, | |
iss: client_email, | |
scope: SCOPES.join(' '), | |
iat, | |
exp, | |
}; | |
const jwt = JWS.sign(null, header, payload, private_key); | |
callback(null, jwt, exp); | |
}); | |
}; | |
const getToken = (serviceAccountKey, callback) => { | |
const now = Math.floor(Date.now() / 1000); | |
if (now + EXPIRES_MARGIN < getEnv(ENV_TOKEN_EXPIRES_AT)) { | |
return callback(); | |
} | |
getJwt(serviceAccountKey, now, (err, jwt, exp) => { | |
if (err) return callback(err); | |
const req = { | |
url: GOOGLE_OAUTH, | |
method: 'POST', | |
header: { | |
'Content-Type': 'application/x-www-form-urlencoded', | |
}, | |
body: { | |
mode: 'urlencoded', | |
urlencoded: [{ | |
key: 'grant_type', | |
value: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | |
},{ | |
key: 'assertion', | |
value: jwt, | |
}], | |
}, | |
}; | |
pm.sendRequest(req, (err, res) => { | |
if (err) return callback(err); | |
const accessToken = res.json().access_token; | |
setEnv(ENV_ACCESS_TOKEN, accessToken); | |
setEnv(ENV_TOKEN_EXPIRES_AT, exp); | |
callback(); | |
}); | |
}); | |
}; | |
const getServiceAccountKey = callback => { | |
try { | |
const keyMaterial = getEnv(ENV_SERVICE_ACCOUNT_KEY); | |
const serviceAccountKey = JSON.parse(keyMaterial); | |
callback(null, serviceAccountKey); | |
} catch (err) { | |
callback(err); | |
} | |
}; | |
getServiceAccountKey((err, serviceAccountKey) => { | |
if (err) throw err; | |
getToken(serviceAccountKey, err => { | |
if (err) throw err; | |
}); | |
}); |
Thanks @ffeldhaus - the original intent for this script is not to call Google Cloud APIs, but to call our own APIs with it. I know the latter is a bit of an anti-pattern, but that's what we used at my workplace (not my decision). Happy to see you got it working with GCP. I can incorporate your changes if you'd like, so we don't have to maintain separate versions - I think then it will work for either use case.
@dinvlad it would be great if you could merge the changes, maybe comment out the Google Cloud scope and just leave it as example. You did already a great job in coming up with this solution and I'll be glad to add something to make it work for even more users.
Hi @dinvlad ,
I'm implementing this script for a project at work. Thanks so much for producing it. The script works fine when I run it manually, but when I try to run it on a schedule via the Postman Cloud I get a weird error. In the manual run, the accessToken and associated environment variable persist outside the pm.sendRequest function where they are generated. When I try to access those variables outside the function after the code has been run, however, my console.log statement says they are undefined and the code doesn't work - I get an auth error because the accessToken I passed in the body to Google Auth was undefined. Once again, this doesn't happen for manual runs of Postman in the app or in their web server, only when I schedule it to run on a regular basis.
Any idea what the problem could be? Let me know if you need any more information.
EDIT: It seems to be the same issue here: postmanlabs/newman#1825
I'm trying proposed solutions on the script and still can't get it to work. I tried timeOut around the pm.sendRequest and that didn't work. I'm going to try to use promises, but may need to review how they work before I get it to run. Any thoughts on how to get the script to work with this error would help, I'm far from an expert at Postman or javascript.
@johnchandlerbaldwin unfortunately no, I haven't tried Postman Cloud, and no ideas beyond what was suggested in those issues..
For sure. I was eventually able to get it to work by splitting out the pm.sendRequest statements into 2 individual requests. Then I was able to schedule it on Postman Cloud. In case anyone has this issue.
Thanks Denis (and everyone else as well ) for being so responsive to everyone's queries.
@johnchandlerbaldwin , just curious to see what did you actually mean when you said split the pm in 2 individual requests. Can you send an excerpt of those 2 splits ? I have a feeling i might be eventually going there
If the code is used for Google Cloud Platform it will not work as the
GOOGLE_OAUTH
constant is pointing to the wrong URI for Google Cloud tokens. At least in the case of Google Cloud Service Account keys, thetoken_uri
field from the service account JSON should be used instead. Also the Google Cloud Scope should be added to make requests work. I have created an updated version of the gist here: https://gist.github.com/ffeldhaus/7753b24cf3631a9ddc1127e6fd835767If someone can check if non Google Cloud Service Accounts also contain the
token_uri
then it should be updated in this gist.