3

I am currently facing the following situation.

Sending Firebase Messages via HttpCall via the google API endpoint:

https://fcm.googleapis.com/v1/projects/projectName/messages:send

Here we have to use OAuth2.0 with a valid Bearer Token like discussed in this question:

What Bearer token should I be using for Firebase Cloud Messaging testing?

After following these steps I was able to send Firebase Messages via the google API.

Now I would like to get the Bearer Token via a HttpCall without doing the manual step with the Playground https://developers.google.com/oauthplayground.

I cannot find any documentation on how to "Exchange authorization code for tokens" via simple HttpCall. I have no possibility to implement any code because I would like to send Firebase messages inside a "Cloud Flow", therefore no possibility to load any external DLL (like the Firebase Admin Dll, which would implement this functionality).

Any help is appreciated

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449

2 Answers2

4

The below code is a Postman Pre-Request Script that is installed on your API collection that contains the routes you are testing. Its purpose is to convert static credentials, like a email-password combination or service account key into an access token to be used with API calls.

Emulate User

To use it for testing on behalf of users, you would add a X-Auth-Token-Type: user header on the request (used & removed by the script below) and you will need to have set up the following environment variables:

Name Value
firebase_apiKey The Firebase API Key for a web application
firebase_test_user An email for an account used for testing
firebase_test_password A password for an account used for testing

Emulate Service Account (Use with caution!)

To use it for testing on behalf of a service account, you would add a X-Auth-Token-Type: admin header on the request (used & removed by the script below) and you will need to have set up the following environment variables:

Name Value
firebase_privateKey The value of private_key in a Service Account Key
Important: For security do not set the "initial value" for this variable!
firebase_scope (optional) A space-delimited list of scopes to authenticate for.
Note: If omitted, the default Admin SDK scopes are used

The Pre-Request Script

const { Header, Response, HeaderList } = require('postman-collection');

/**
 * Information about the current Firebase user
 * @typedef {Object} UserInfo
 * @property {String} accessToken - The Firebase ID token for this user
 * @property {String | undefined} displayName - Display name of the user, if available
 * @property {Number} expiresAt - When this token expires as a unix timestamp
 * @property {String | undefined} email - Email associated with the user, if available
 * @property {String} refreshToken - Refresh token for this user's ID token
 * @property {String} uid - User ID for this user
 */

/**
 * Loads a third-party JavaScript module from a CDN (e.g. unpkg, jsDelivr)
 * @param {[String, String, String]} moduleTuple - Array containing the module's ID, its source URL and an optional SHA256 signature
 * @param {Object | (err: any, exports: any) => any} exportsRefOrCallback - Object reference to use as `exports` for the module or a result handler callback
 * @param {(err: any, exports: any) => any} callback - result handler callback
 */
function loadModule(moduleTuple, exportsRefOrCallback, callback = undefined) {
    const exports = arguments.length == 2 ? {} : exportsRefOrCallback;
    callback = arguments.length == 2 ? exportsRefOrCallback : callback;
    const [id, src, signature] = moduleTuple;
   
    if (pm.environment.has("jslibcache_" + id)) {
        const script = pm.environment.get("jslibcache_" + id);

        if (signature && signature === CryptoJS.SHA256(script).toString()) {
            console.log("Using cached copy of " + src);
            try {
              eval(script);
              return callback(null, exports);
            } catch {}
        }
    }

    pm.sendRequest(src, (err, response) => {
        try {
            if (err || response.code !== 200) {
                pm.expect.fail('Could not load external library');
            }

            const script = response.text();
            signature && pm.expect(CryptoJS.SHA256(script).toString(), 'External library (' + id + ') has a bad SHA256 signature').to.equal(signature);
            pm.environment.set("jslibcache_" + id, script);
            eval(script);

            callback(null, exports);
        } catch (err) {
            callback(err, null);
        }
    });
}

/**
 * Signs in a test user using an email and password combination
 * 
 * @param {String} email email of the account to sign in with
 * @param {String} password email of the account to sign in with
 * @param {(error: any, response: Response) => any} callback request result handler
 */
function signInWithEmailAndPassword(email, password, callback) {
    pm.sendRequest({
        url: "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=" + encodeURIComponent(pm.environment.get("firebase_apiKey")),
        body: JSON.stringify({ email, password, "returnSecureToken": true }),
        headers: new HeaderList({}, [new Header("application/json", "Content-Type")]),
        method: "POST"
    }, callback);
}

/**
 * Builds an Admin SDK compatible JWT using a Service Account key
 * 
 * Required Environment Variables:
 *  - `firebase_privateKey` - the private key from inside a service account key JSON file
 * 
 * Environment Variables:
 *  - `firebase_scope` - scopes used for the access token, space delimited
 * 
 * @param {Boolean | (error: any, idToken: String) => any} callbackOrForceRefresh token result handler or `true` to force using a fresh user token
 * @param {(error: any, idToken: String) => any} [callback] token result handler
 */
function getAdminToken(callbackOrForceRefresh, callback) {
    let forceRefresh = Boolean(callbackOrForceRefresh);
    if (arguments.length === 1) {
        callback = callbackOrForceRefresh;
        forceRefresh = callbackOrForceRefresh = false;
    }

    loadModule(
        ["jsrsasign", "https://unpkg.com/jsrsasign@10.3.0/lib/jsrsasign.js", "39b7a00e9eed7d20b2e60fff0775697ff43160e02e5276868ae8780295598fd3"],
        (loadErr, { KJUR }) => {
            if (loadErr) return callback(loadErr, null);
            
            const exp = pm.environment.get("currentAdmin.exp");
            const nowSecs = Math.floor(Date.now() / 1000);

            if (exp && exp > nowSecs && forceRefresh === false) {
                return callback(null, pm.environment.get("currentAdmin.jwt"));
            }

            try {
                if (!pm.environment.has('firebase_privateKey')) {
                    pm.expect.fail('Missing required environment variable "firebase_privateKey".');
                }

                // use specified scopes, or fallback to Admin SDK defaults
                const scope = pm.environment.get('firebase_scope') || 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/firebase.database https://www.googleapis.com/auth/firebase.messaging https://www.googleapis.com/auth/identitytoolkit https://www.googleapis.com/auth/userinfo.email';
                const privateKey = String(pm.environment.get('firebase_privateKey')).replace("\\n", "\n");

                const header = {"alg" : "RS256", "typ" : "JWT"};
                
                const claimSet =
                {
                    "iss": "https://securetoken.google.com/" + pm.environment.get("firebase_projectId"),
                    "scope": scope,
                    "aud":"https://accounts.google.com/o/oauth2/auth",
                    "exp": nowSecs + 3600, // now + 1 hour
                    "iat": nowSecs
                }

                const jwt = KJUR.jws.JWS.sign(null, header, claimSet, privateKey);
                
                // comment these lines out to disable caching
                pm.environment.set("currentAdmin.jwt", jwt);
                pm.environment.set("currentAdmin.exp", claimSet.exp);

                callback(null, jwt);
            } catch (err) {
                callback(err, null);
            }
        }
    );
}

/**
 * Builds a User ID Token using an email-password combo
 * 
 * Required Environment Variables:
 *  - `firebase_apiKey` - the Firebase API key for a web application
 *  - `firebase_test_user` - an email for a test user
 *  - `firebase_test_password` - the password for the test user
 * 
 * @param {Boolean | (error: any, idToken: String) => any} callbackOrForceRefresh token result handler or `true` to force using a fresh user token
 * @param {(error: any, idToken: String) => any} [callback] token result handler
 */
function getIdToken(callbackOrForceRefresh, callback) {
    let forceRefresh = Boolean(callbackOrForceRefresh);
    if (arguments.length === 1) {
        callback = callbackOrForceRefresh;
        forceRefresh = callbackOrForceRefresh = false;
    }

    if (pm.environment.has("currentUser") && forceRefresh === false) {
        /** @type UserInfo */
        const currentUser = JSON.parse(pm.environment.has("currentUser"));
        if (currentUser.expiresAt > Date.now()) { // has token expired?
            return callback(null, currentUser.accessToken);
        }
    }

    try {
        if (!pm.environment.has('firebase_apiKey')) {
            pm.expect.fail('Missing required environment variable "firebase_apiKey".');
        }
        if (!pm.environment.has('firebase_test_user')) {
            pm.expect.fail('Missing required environment variable "firebase_test_user".');
        }
        if (!pm.environment.has('firebase_test_password')) {
            pm.expect.fail('Missing required environment variable "firebase_test_password".');
        }
    } catch (err) {
        return callback(err, null);
    }

    signInWithEmailAndPassword(pm.environment.get("firebase_test_user"), pm.environment.get("firebase_test_password"), (err, response) => {
        if (err || response.code !== 200) {
            pm.expect.fail('Could not sign in user: ' + response.json().error.message);
        }

        /** @type String */
        let accessToken;

        try {
            const { idToken, refreshToken, email, displayName, localId: uid, expiresIn } = response.json();
            accessToken = idToken;
            const expiresAt = Date.now() + Number(expiresIn);

            // comment these lines out to disable caching
            pm.environment.set("currentUser", JSON.stringify({ accessToken, refreshToken, email, displayName, uid, expiresAt }));
            // pm.environment.set("currentUser.accessToken", accessToken);
            // pm.environment.set("currentUser.refreshToken", refreshToken);
            // pm.environment.set("currentUser.email", email);
            // pm.environment.set("currentUser.displayName", displayName);
            // pm.environment.set("currentUser.uid", uid);
            // pm.environment.set("currentUser.expiresAt", expiresAt);

        } catch (err) {
            return callback(err, null);
        }

        callback(null, accessToken);
    });
}

const tokenTypeHeader = pm.request.headers.one("X-Auth-Token-Type");
pm.request.removeHeader("X-Auth-Token-Type");

switch (tokenTypeHeader && tokenTypeHeader.value.toLowerCase()) {
    case "admin":
        getAdminToken(false, (err, token) => { 
            if (err || !token) pm.expect.fail("failed to get admin SDK token for request: " + err.message);
            pm.request.addHeader(new Header("Bearer " + token, "Authorization"));
        });
        break;
    case "user":
        getIdToken(false, (err, idToken) => {
            if (err || !idToken) pm.expect.fail("failed to get user ID token for request: " + err.message);
            pm.request.addHeader(new Header("Bearer " + idToken, "Authorization"));
        });
        break;
    default:
        break; // no auth, do nothing
}
Lucas Sousa
  • 401
  • 5
  • 18
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • Thank you for this Pre-Request Script. I already tried some of this stuff and was able to get the desired token. My problem still remains because I cannot call a script directly, I just want the api Call without loading external libraries. "I have no possibility to implement any code because I would like to send Firebase messages inside a "Cloud Flow", therefore no possibility to load any external DLL (like the Firebase Admin Dll, which would implement this functionality)." – Mario Köstl Jun 30 '21 at 08:37
  • @MarioKöstl Your question requested a solution for Postman that allows you to skip obtaining OAuth tokens manually. As you didn't specify what "Cloud Flow" is, I assumed you had Postman running as some part of CI process. If you are actually looking for a solution that can send messages using a "webhook" or simple REST API, ask a [new question](https://stackoverflow.com/questions/ask) stating as such. – samthecodingman Jun 30 '21 at 13:04
  • You are right, my question would assume that I am looking for a solution for Postman, but I am simple using postman to test the Web API Calls. I rephrased my question (https://stackoverflow.com/questions/68197714/getting-firebase-bearer-token-by-simple-httpcall-rest-api). Thank you for your solution, I am definitely using this Pre-Request Script for other Projects. – Mario Köstl Jun 30 '21 at 15:34
0

You can obtain a valid Bearer Token from the OAuth access token with your firebase service account. Using your Service Account credentials from your Firebase console. If it is at all possible within your environment, I suggest using the OAuth 2 options than you can find here: https://firebase.google.com/docs/database/rest/auth#authenticate_with_an_access_token

Otherwise, You will then have to mint the credentials which will provide an access token which will be a valid bearer token.

It should be noted that this is only available in the following languages:

  • node.js
  • python
  • java

https://firebase.google.com/docs/cloud-messaging/auth-server#use-credentials-to-mint-access-tokens

DIGI Byte
  • 4,225
  • 1
  • 12
  • 20
  • I already tried this stuff, and it is working. It is no solving my problem. "I have no possibility to implement any code because I would like to send Firebase messages inside a "Cloud Flow", therefore no possibility to load any external DLL (like the Firebase Admin Dll, which would implement this functionality)." – Mario Köstl Jun 30 '21 at 08:40