Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Auto-generate Google Access and ID tokens from a Service Account key and save it in Postman
/* 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;
});
});
@david-obadofin
Copy link

david-obadofin commented Feb 13, 2021

@dinvlad this worked! thanks

@juanrestrepo7716
Copy link

juanrestrepo7716 commented May 6, 2021

@dinvlad hello, thank you very much it was a great help, does anyone know how to implement this in angular?

@jgmsteinfeld
Copy link

jgmsteinfeld commented Sep 15, 2021

Dinvlad, trying to use this and getting the following error:

GET https://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js
Error: unable to get local issuer certificate
Request Headers
User-Agent: PostmanRuntime/7.28.4
Accept: /
Postman-Token: 2496599a-8acd-4ed5-af6f-35b294062109
Host: kjur.github.io
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

@dinvlad
Copy link
Author

dinvlad commented Sep 15, 2021

@jgmsteinfeld not sure tbh - it's working for me. Perhaps there's a problem with your certificates (or a temporary server-side problem)? If you're on Linux, you may need to install ca-certificates.

@jgmsteinfeld
Copy link

jgmsteinfeld commented Sep 16, 2021

Thx Dinvlad. Let me give you a bit more context on this. I added my entire serviceAccountKey JSON to the parameter "serviceAccountKey" variable in an Environment tied to my request. The request I am making is to the "https://cloudasset.googleapis.com/v1/projects/<>:exportAssets" GCP API, but that is not happening as of yet as the error appears to be in the Pre-request Script. For Token I have this set to {{accessToken}} which I believe is correct.

@dinvlad
Copy link
Author

dinvlad commented Sep 16, 2021

@jgmsteinfeld I'm not sure tbh, everything seems to work fine on my end at least. As mentioned, it really does look like your Postman is missing some certificates and you may need to import them. Depending on your OS and if you're behind a corporate proxy, there might be a certificate bundle you may need to import - please see https://community.postman.com/t/unable-to-get-local-issuer-certificate-error/12323/5

@jgmsteinfeld
Copy link

jgmsteinfeld commented Sep 16, 2021

@dinvlad Thank you very much the article told me where the issue was. I needed to turn off "SSL certificate verification" for making the GCP call. Now that error is gone. Thank you!. I did need to add the right scope for the API, but once done it worked like a charm. Thank you again.

@Totti10as
Copy link

Totti10as commented Sep 29, 2021

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?

@dinvlad
Copy link
Author

dinvlad commented Sep 29, 2021

@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
Copy link

Totti10as commented Sep 30, 2021

@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.

@dinvlad
Copy link
Author

dinvlad commented Sep 30, 2021

@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..

@dinvlad
Copy link
Author

dinvlad commented Sep 30, 2021

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.

@Totti10as
Copy link

Totti10as commented Oct 3, 2021

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 ):
image

also the "https://www.googleapis.com/auth/gmail.modify" has been added as well under the scope
and the domain delegation activated:
image

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 (:

@Totti10as
Copy link

Totti10as commented Oct 3, 2021

@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.

@dinvlad
Copy link
Author

dinvlad commented Oct 3, 2021

Thank you @Totti10as for digging into it! As suspected, GSuite is quite restrictive.

@NirajCricket
Copy link

NirajCricket commented Oct 12, 2021

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,

  1. It says to have a admin permission for remote config to this SA, hence does it need to be in that role?
  2. 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:
    image

@dinvlad
Copy link
Author

dinvlad commented Oct 12, 2021

@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.

@silvioangels
Copy link

silvioangels commented Oct 27, 2021

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?

@dinvlad
Copy link
Author

dinvlad commented Oct 27, 2021

@silvioangels are you using idToken for your Bearer header to GCF?

@dinvlad
Copy link
Author

dinvlad commented Oct 27, 2021

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).

@silvioangels
Copy link

silvioangels commented Oct 27, 2021

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,

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;
});

});

@silvioangels
Copy link

silvioangels commented Oct 27, 2021

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
}

@dinvlad
Copy link
Author

dinvlad commented Oct 27, 2021

@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

@dinvlad
Copy link
Author

dinvlad commented Oct 27, 2021

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.

@silvioangels
Copy link

silvioangels commented Oct 27, 2021

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.

@dinvlad
Copy link
Author

dinvlad commented Oct 28, 2021

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.

@silvioangels
Copy link

silvioangels commented Oct 29, 2021

@dinvlad it´s not a plugin, it´s the google sdk

@JSLadeo
Copy link

JSLadeo commented Dec 6, 2021

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:
image
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;
});

});

@dinvlad
Copy link
Author

dinvlad commented Dec 7, 2021

@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"
}

@JSLadeo
Copy link

JSLadeo commented Dec 7, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment