-
-
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; | |
}); | |
}); |
OK - if you can post the (masked) contents of your JWT payload (without headers/signature) both from this plugin and from print-identity-token, it would help understanding how it's different between these two.
@dinvlad it´s not a plugin, it´s the google sdk
Hello , thanks a lot for this script, but for me it doesnt work.
I use for the scope : 'https://www.googleapis.com/auth/devstorage.read_only'.
My environment detail are:
detail of my service account key:
{"web":{"client_id":"---------.apps.googleusercontent.com",
"project_id":"------",
"auth_uri":"https://accounts.google.com/o/oauth2/auth",
"token_uri":"https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
"client_secret":"*************"}}
1st error: when i launch the request, i got an error " Cannot read property 'curve' of undefined", i think its cause of import "jsrsasign-latest-all-min.js" so i import before in another request
2nd error: After i import js rsa in my environment, i get an error in prescript but with no detail of the error...
If you can help me, can be awesome (it work well with manually token)
copy of the prescript :
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 JS_RSA_SIGN_SRC = 'https://raw.githubusercontent.com/kjur/jsrsasign/master/jsrsasign-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/devstorage.read_only'
];
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;
});
});
@JSLadeo that service account key looks very strange, how did you generate it? It needs to be generated using Google Cloud IAM Console or Google Cloud SDK.. https://cloud.google.com/iam/docs/creating-managing-service-account-keys
For example, mine look like this
{
"client_email": "sa_name@project_id.iam.gserviceaccount.com",
"client_id": "1234xxxxyyyyzzzzaaaabbbb",
"private_key": "-----BEGIN PRIVATE KEY-----\n<REDACTED>\n-----END PRIVATE KEY-----\n",
"private_key_id": "a1234567890abcdef1234567890abcdef>",
"token_uri": "https://oauth2.googleapis.com/token",
"type": "service_account"
}
Hello , thx for your answer, got no error now, just 404 "No such object: -------remote-storage/--------.fic" . I work on it and tell you how it evolve
edit: probleme solve with good service account key.
Tanks a lot
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, the token_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/7753b24cf3631a9ddc1127e6fd835767
If someone can check if non Google Cloud Service Accounts also contain the token_uri
then it should be updated in this gist.
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
I got it. Our GCP Architect told us the autentication is this documentation:
https://developers.google.com/identity/protocols/oauth2/service-account?hl=en,
but he can´t provide a postman example of it (generating the bear token), if i use the gcloud comands ("auth activate-service-account --key-file" and "auth print-identity-token") i can call the GCF with the bear token.
i think the original pre_request.js that you did is the right way, but i´m trying here all options.
About generate the id_token, it´s blocked in GCF this way.