Skip to content

Instantly share code, notes, and snippets.

@dinvlad
Last active April 11, 2024 02:57
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save dinvlad/425a072c8d23c1895e9d345b67909af0 to your computer and use it in GitHub Desktop.
Save dinvlad/425a072c8d23c1895e9d345b67909af0 to your computer and use it in GitHub Desktop.
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;
});
});
@silvioangels
Copy link

@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

@ffeldhaus
Copy link

ffeldhaus commented Aug 29, 2022

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.

@dinvlad
Copy link
Author

dinvlad commented Aug 29, 2022

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.

@ffeldhaus
Copy link

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

@johnchandlerbaldwin
Copy link

johnchandlerbaldwin commented Jun 30, 2023

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.

@dinvlad
Copy link
Author

dinvlad commented Jul 2, 2023

@johnchandlerbaldwin unfortunately no, I haven't tried Postman Cloud, and no ideas beyond what was suggested in those issues..

@johnchandlerbaldwin
Copy link

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.

@mnikhil7692
Copy link

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

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