Skip to content

Instantly share code, notes, and snippets.

@dinvlad
Last active January 9, 2024 09:32
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

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

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

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