-
-
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; | |
}); | |
}); |
@Totti10as please see an earlier comment on how another user was able to get it working with GMail (and you need to enable domain-wide delegation for that service account).
@Totti10as please see an earlier comment on how another user was able to get it working with GMail (and you need to enable domain-wide delegation for that service account).
I did both as @IGrimaylo and you @dinvlad suggested but now getting same error as @IGrimaylo describe above.
@Totti10as I'm not too sure then - if you could share a Gist with your modified code I could take a look, otherwise it might just "not work" with Gmail.. Google is more stringent with these APIs as they can be easily abused by machine accounts..
Also @Totti10as, the error clearly indicates the scopes are not sufficient. Could you confirm you're using GMail APIs, if and if so, which operation(s) are you trying to accomplish? I think you may need to add "https://www.googleapis.com/auth/gmail.modify"
scope, if you haven't already.
Also @Totti10as, the error clearly indicates the scopes are not sufficient. Could you confirm you're using GMail APIs, if and if so, which operation(s) are you trying to accomplish? I think you may need to add
"https://www.googleapis.com/auth/gmail.modify"
scope, if you haven't already.
@dinvlad i've added only one parameter to your script under the payload as is (other than that, nothing has changed, the token itself received properly and script seems to be work as expected ):
also the "https://www.googleapis.com/auth/gmail.modify" has been added as well under the scope
and the domain delegation activated:
and the request itself contains client email and not the service account;
https://gmail.googleapis.com/gmail/v1/users/{{gmailUserAcc}}/messages
i think it can cause the issue...
but in other hand I've tried to add the SA email in the request and still got same error (:
@dinvlad
After all it seams that Gmail api does not support service accounts for non gsuite domains.
You can only use a service account with a Gsuite domain account and gsuite domain emails.
Thank you @Totti10as for digging into it! As suspected, GSuite is quite restrictive.
Hi @dinvlad, I was searching for this article. thanks for uploading this.
However, postman has made some changes in variables that seem like.
I am trying to setup this to Googleapi via postman, and want to automate these tasks, as googleapis says to edit the remoteconfig, it needs a Service account. https://firebase.google.com/docs/remote-config/automate-rc#get_an_access_token_to_authenticate_and_authorize_api_requests.
I tried following your article , and I do got the JSON file from the service account, but as per google article,
- It says to have a admin permission for remote config to this SA, hence does it need to be in that role?
- second, can you confirm if we need the Environment to setup or if I just edit my collection and put variables in that collection, I did configure the variables in Env, but unable to call them in collection, whereas if I configure them in collection, I can see them:
@NirajCricket not sure tbh, everything still works on my end I think. If it works with collections, great! You'd need to add Firebase Remote Config Admin
role for your SA in any case, however.
Guys, i´m trying to call the Cloud functions service.
It´s giving to me the following error:
{
"message": "Jwt is not in the form of Header.Payload.Signature with two dots and 3 sections",
"code": 401
}
I just updated the scope variable to:
const SCOPES = [
'https://www.googleapis.com/auth/cloud-platform'
];
Do you have any idea of whats going on?
@silvioangels are you using idToken
for your Bearer header to GCF?
and afaik you also would need to set a specific audience as explained here https://gist.github.com/dinvlad/425a072c8d23c1895e9d345b67909af0#gistcomment-3629784, but specific to GCF (depending on which JWT auth you're using for your GCF - either built-in IAM auth or Firebase Auth, the audiences will be different in each case).
I changed now to idToken, but i don´t know what value can i set in the target_audience variable. Where can i found this?
I tried put the same url that i´m calling, but gives me the same 401 error.
I´ll put here the code, just to be sure that i´m setting the right code on "Pre-request Script" section:
/* 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
- postmanlabs/postman-app-support#1607 (comment)
*/
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/cloud-platform'
];
const EXPIRES_MARGIN = 1; // 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 = {
target_audience: '<my url here>',
aud: GOOGLE_OAUTH,
iss: client_email,
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);
const idToken = res.json().id_token;
setEnv("idToken", idToken);
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;
});
});
Wow @dinvlad i not change the bear token, now he give me the 403 error, i´m not understanding wha´s wrong :-\
{
"message": "Audiences in Jwt are not allowed",
"code": 403
}
@silvioangels if you're using IAM auth for your GCF (as it looks like), then the audience should be the URL of your function. You can read more about it here, and I suggest first invoking your function with a manually generated token as explained there: https://cloud.google.com/functions/docs/securing/authenticating
And if that doesn't work for you, try copy-pasting idToken
to https://jwt.io, and comparing that to the manually generated token via gcloud auth print-identity-token
.
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.
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
Hi @dinvlad,
I've following that instruction
section - Configuring Postman to use a pre-request script and service credentials.
and using your code but once trying to login got 403:
{
"error": {
"code": 403,
"message": "Request had insufficient authentication scopes.",
"errors": [
{
"message": "Insufficient Permission",
"domain": "global",
"reason": "insufficientPermissions"
}
],
"status": "PERMISSION_DENIED"
}
}
Scope:
// add/remove your own scopes as needed
const SCOPES = [
'https://mail.google.com/',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cloud-platform'
];
Could you please advice?