Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrating from legacy FCM APIs to HTTP v1 #238

Closed
dlcole opened this issue Dec 6, 2023 · 3 comments
Closed

Migrating from legacy FCM APIs to HTTP v1 #238

dlcole opened this issue Dec 6, 2023 · 3 comments

Comments

@dlcole
Copy link

dlcole commented Dec 6, 2023

TLDR; I ultimately used Firebase Hosting to deploy server code to invoke the HTTP v1 API. See dlcole/httpV1Msg for a sample implementation.

I’m creating this issue to track the changes necessary to migrate to firebase's new HTTP v1 messaging API, as required by June 20, 2024. My app was originally built with the older nativescript/firebase plugin which had excellent documentation on how to send and receive notifications via Firebase Cloud Messaging (FCM) and http POST's. Google does have the document, Migrate from legacy FCM APIs to HTTP v1, but it is WOEFULLY inadequate to document the tasks actually needed to migrate, and the newer firebase plugin @nativescript/firebase-messaging readme while good, doesn't offer much more when it comes to this topic.

What documentation there is suggests that sending notifications via server code is preferable to the simple http POST in FCM. There are code snippets, but not actual working examples, of what such server code should look like, especially within a Nativescript app.

My preference is to continue to use http POST's, as I can obscure my private key if necessary, and I don't see that as being inherently less secure than the server ID I'm currently using with legacy FCM. I found this tutorial to be VERY helpful, but it, too, is incomplete, as the bearer authentication needs to be recreated with each use. (This is worth watching just to get commiseration on how inadequate the Google documentation is.)

The resulting code should look something like this:

const jwt = require('jsonwebtoken');

// Set the necessary claims for the custom token
const uid = 'your-user-id'; // user.uid returned from firebase login 
const claims = {
  uid: uid,
  iat: Math.floor(Date.now() / 1000), // Issued at claim (current time in seconds)
  // Add other custom claims if needed
};

// Generate the custom token using your Firebase project's service account private key
const privateKey = `-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----`; // Replace with the private key from downloaded json file

const customToken = jwt.sign(claims, privateKey, { algorithm: 'RS256' });

With this you should have what you need to add the authorization to the HTTP v1 POST:

Authorization: Bearer <customToken>

Sounds easy, right? Well, when I simply require jsonwebtoken, I receive these 6 error messages on Android:

ERROR in node:buffer
Module build failed: UnhandledSchemeError: Reading from "node:buffer" is not handled by plugins (Unhandled scheme).

ERROR in node:crypto
Module build failed: UnhandledSchemeError: Reading from "node:crypto" is not handled by plugins (Unhandled scheme).

ERROR in node:events
Module build failed: UnhandledSchemeError: Reading from "node:events" is not handled by plugins (Unhandled scheme).

ERROR in node:https
Module build failed: UnhandledSchemeError: Reading from "node:https" is not handled by plugins (Unhandled scheme).

ERROR in node:http
Module build failed: UnhandledSchemeError: Reading from "node:http" is not handled by plugins (Unhandled scheme).

ERROR in node:util
Module build failed: UnhandledSchemeError: Reading from "node:util" is not handled by plugins (Unhandled scheme).

This looks unsettlingly similar to the issues I saw with WebPack 5 (see this related issue from last August, with the webpack.config.js changes necessary to fix). I tried using the jose plugin an alternative but received the same error messages.

So, that's where I'm at. If I can resolve these errors messages I think I'll be unstuck. My challenge is translating what solutions I find online to what would be specified webpack.config.js. Yes, I've ready the docs many, many times and it's still alien to me. When I can better articulate a question I'll post that separately.

For completeness, here's the app's current package.json:

{
  "name": "festivelo",
  "main": "app/app.js",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@bradmartin/nativescript-urlhandler": "^2.0.1",
    "@kefah/nativescript-google-maps": "^1.0.7",
    "@master.technology/permissions": "^2.0.1",
    "@nativescript-community/ui-material-bottom-navigation": "^7.2.21",
    "@nativescript/appversion": "^2.0.0",
    "@nativescript/contacts": "^2.1.0",
    "@nativescript/core": "^8.6.1",
    "@nativescript/email": "^2.1.0",
    "@nativescript/firebase-auth": "^3.2.0",
    "@nativescript/firebase-core": "^3.2.0",
    "@nativescript/firebase-database": "^3.2.0",
    "@nativescript/firebase-messaging": "^3.2.0",
    "@nativescript/firebase-ui": "^3.2.0",
    "@nativescript/geolocation": "^8.3.1",
    "@nativescript/iqkeyboardmanager": "^2.1.1",
    "@nativescript/theme": "^3.0.2",
    "@triniwiz/nativescript-toasty": "^4.1.3",
    "assert": "^2.1.0",
    "base-64": "^1.0.0",
    "browserify-zlib": "^0.2.0",
    "buffer": "^6.0.3",
    "crypto-browserify": "^3.12.0",
    "https-browserify": "^1.0.0",
    "jsonwebtoken": "^9.0.2",
    "nativescript-app-tour": "^4.0.0",
    "nativescript-bitmap-factory": "^1.8.1",
    "nativescript-clipboard": "^2.1.1",
    "nativescript-danem-google-maps-utils": "^1.0.18",
    "nativescript-drop-down": "6.0.2",
    "nativescript-pdf-view": "^3.0.0-1",
    "nativescript-phone": "^3.0.3",
    "nativescript-screenshot": "^0.0.2",
    "nativescript-sqlite": "^2.8.6",
    "nativescript-ui-listview": "^15.2.3",
    "nativescript-ui-sidedrawer": "^15.2.3",
    "os-browserify": "^0.3.0",
    "patch-package": "^8.0.0",
    "path": "^0.12.7",
    "path-browserify": "^1.0.1",
    "querystring-es3": "^0.2.1",
    "stream-browserify": "^3.0.0",
    "stream-http": "^3.2.0",
    "tty-browserify": "^0.0.1",
    "url": "^0.11.3"
  },
  "devDependencies": {
    "@nativescript/android": "8.6.2",
    "@nativescript/ios": "8.6.3",
    "@nativescript/webpack": "^5.0.18",
    "util": "^0.12.5"
  },
  "scripts": {
    "postinstall": "patch-package"
  }
}

And webpack.config.js

const webpack = require("@nativescript/webpack");

module.exports = (env) => {

  // See https://stackoverflow.com/questions/70428159/nativescript-8-1-migration-adding-external-to-webpack-config-js
  env.appComponents = (env.appComponents || []).concat(['./foreground-service.android'])
	
  webpack.init(env);

	// Learn how to customize:
	// https://docs.nativescript.org/configuration/webpack

  // Required so that includes from symklinks will work 
  // See https://github.com/vercel/next.js/issues/3168
  webpack.mergeWebpack({ 
    resolve: {
      symlinks: false,
    }
  })

  // To watch for changes in symlinks - see https://webpack.js.org/configuration/watch/
  webpack.chainWebpack((config) => {
    config.watchOptions({
        followSymlinks: true,
    });
  });

  webpack.Utils.addCopyRule('data/**')
  webpack.Utils.addCopyRule('files/**')
  webpack.Utils.addCopyRule('**/*.db')

  webpack.mergeWebpack({
    resolve: {
      fallback: {
        assert: require.resolve("assert/"),
        browserify: false,
        buffer: require.resolve("buffer/"),
        child_process: false, 
        crypto: require.resolve("crypto-browserify"), 
        dns: false,
        fs: false,
        http: require.resolve("stream-http"),
        https: require.resolve("https-browserify"),
        http2: false,
        net: false,
        os: require.resolve("os-browserify/browser"),
        path: require.resolve("path-browserify"),
        querystring: require.resolve("querystring-es3"),
        request: false,
        stream: require.resolve("stream-browserify"),
        tls: false,
        tty: require.resolve("tty-browserify"),
        url: require.resolve("url/"),
        util: require.resolve("util/"),
        zlib: require.resolve("browserify-zlib"),
      },
    },
  });

  return webpack.resolveConfig();
};

Edit 12/08/2023:

After two days of investigation and trial-and-error, this addition to webpack.config.js takes care of the 6 build errors I was seeing:

const { NormalModuleReplacementPlugin } = require('webpack');

webpack.chainWebpack((config) => {
    config.plugin('NormalModuleReplacementPlugin')
      .use(new NormalModuleReplacementPlugin(/^node:/, (resource) => {
        resource.request = resource.request.replace(/^node:/, "");
      }))
  })

But I'm not unstuck, as I now get the runtime error:

TypeError: Cannot read properties of undefined (reading 'env')
StackTrace:
  System.err: ./node_modules/util/util.js(file: app/webpack:/festivelo/node_modules/util/util.js:109:11)

This appears to be the same problem identified in this issue and this issue, but so far none of the solutions suggested there resolve the issue.

All this is to be able to invoke jsonwebtoken.sign(), and right now it's only a hopeful guess that that will actually create a useable Authorization: Bearer token.

Edit 12/09/2023:

I received a suggestion from one of the referenced issues, above, and this addition to webpack.config.js eliminates the env errors, but it's hard to say if it might cause other problems.

  webpack.chainWebpack((config) => {

    // See https://github.com/NativeScript/android/issues/1772
    config.plugin('DefinePlugin').tap(args => {
      Object.assign(args[0], {
        "process": JSON.stringify('HELLO'),
        "process.env": JSON.stringify('HELLO'),
      })
      return args
    })

And after that, I resolved a Buffer reference error with

    // https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined
    config.plugin('ProvidePlugin')
      .use(new ProvidePlugin({
        Buffer: ['buffer', 'Buffer'],
        process: 'process/browser',
      }),
      )

I'll be honest, I've never seen a rabbit hole this deep. jsonwebtoken is intended for a node environment and it takes all kinds of shenanigans in webapck.config.js to knock down one error after another. I have no idea how many errors might remain, or if jsonwebtoken is even the right utility. (It was suggested by nextpage.ai, but it might have been hallucinating.) I've found other documentation that suggests google-auth as the means to create the bearer token, but it is also intended for a node environment, as is jose. After a full week on this I have nothing to show, except that I've learned more about webpack than I really wanted to know.

I'll ask for help in Discord and hope someone can throw me a bone.

Edit 12/10/2023:

Here’s where things currently stand... From the code at the top of this description, when I run:

try {
  const customToken = jwt.sign(claims, privateKey, { algorithm: 'RS256' });
  console.log("customToken: " + JSON.stringify(customToken, null, 4));
} catch(e) {
  console.log("error: " + e);
}

I catch the error

TypeError: crypto.createSign is not a function

I can’t step into jwt.sign in a debugger, and while I’ve spent a lot of time digging through the jsonwebtoken and crypto-browserify source, I’m not making any progress. I expect webpack.config.js is pertinent, so here’s my current file:

const webpack = require("@nativescript/webpack");
const { NormalModuleReplacementPlugin } = require('webpack');
const { ProvidePlugin } = require('webpack');

module.exports = (env) => {

  // See https://stackoverflow.com/questions/70428159/nativescript-8-1-migration-adding-external-to-webpack-config-js
  env.appComponents = (env.appComponents || []).concat(['./foreground-service.android'])

  webpack.init(env);

  // Learn how to customize:
  // https://docs.nativescript.org/configuration/webpack

  // Required so that includes from symklinks will work 
  // See https://github.com/vercel/next.js/issues/3168
  webpack.mergeWebpack({
    resolve: {
      symlinks: false,
    }
  })

  // To watch for changes in symlinks - see https://webpack.js.org/configuration/watch/
  webpack.chainWebpack((config) => {
    config.watchOptions({
      followSymlinks: true,
    });
  });

  webpack.chainWebpack((config) => {

    // See https://github.com/NativeScript/android/issues/1772
    config.plugin('DefinePlugin').tap(args => {
      Object.assign(args[0], {
        "process": JSON.stringify('HELLO'),
        "process.env": JSON.stringify('HELLO'),
      })
      return args
    })

    // https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined
    config.plugin('ProvidePlugin')
      .use(new ProvidePlugin({
        Buffer: ['buffer', 'Buffer'],
        process: 'process/browser',
      }),
      )

    // See https://discord.com/channels/603595811204366337/751068755206864916/1136575416426057780
    config.resolve.alias.set('inherits$', 'inherits/inherits_browser');

    // See https://github.com/vercel/next.js/issues/28774
    config.plugin('NormalModuleReplacementPlugin')
      .use(new NormalModuleReplacementPlugin(/^node:/, (resource) => {
        resource.request = resource.request.replace(/^node:/, "");
      }))
  })

  webpack.Utils.addCopyRule('data/**')
  webpack.Utils.addCopyRule('files/**')
  webpack.Utils.addCopyRule('**/*.db')

  webpack.mergeWebpack({
    resolve: {
      fallback: {
        assert: require.resolve("assert/"),
        browserify: false,
        "browserify-sign": require.resolve("browserify-sign"),
        // sign: require.resolve("browserify-sign"),
        buffer: require.resolve("buffer/"),
        child_process: false,
        crypto: require.resolve("crypto-browserify"),
        "crypto-browserify": require.resolve("crypto-browserify"),
        dns: false,
        fs: false,
        http: require.resolve("stream-http"),
        https: require.resolve("https-browserify"),
        http2: false,
        net: false,
        os: require.resolve("os-browserify/browser"),
        path: require.resolve("path-browserify"),
        process: require.resolve("process"),
        // 'process/browser': require.resolve('process/browser'), 
        querystring: require.resolve("querystring-es3"),
        request: false,
        stream: require.resolve("stream-browserify"),
        tls: false,
        tty: require.resolve("tty-browserify"),
        url: require.resolve("url/"),
        util: require.resolve("util/"),
        zlib: require.resolve("browserify-zlib"),
      },
    },
  });

  return webpack.resolveConfig();
};

I’ve got other oceans to boil, notably migrating from @kefah/nativescript-google-maps to @nativescript/google-maps, so I plan to redirect my efforts there for the time being, hoping perhaps others can make progress on this front.

I’ll be back.

Edit 12/20/2023:

To their belated credit, firebase has added documentation about how to set authorization. Here's the full example. But it still presumes a node server environment, and the line const { google } = require('googleapis'); gave me LOTS of runtime errors.

You beat your head against a wall long enough and you'll think maybe you should at least try a different wall. To that end, I took the approach writing a node server that would contain the service account JSON file, perform the authentication, and send the message. This turned out to be no small effort, but I can now successfully send push notifications via the HTTP v1 API to a topic. Sending to a device is next, but that should be straightforward at this point.

My takeaway from all this is that if you're currently using server code to send FCM messages, then changing that to send via HTTP v1 should be relatively easy. If you're sending your messages from within your app, however, then you have a major migration effort ahead. I've spent about 3 weeks on this so far.

I plan to create a GitHub repository that contains my server code and client driver to help other fellow travelers along this path. I'll update this issue when I have more details.

Edit 12/21/2023:

I spoke too early. Sending a push notification via the node server is, for now, problematic. SOMETIMES it will work, but right now I'm consistently hitting the error

Error: secretOrPrivateKey must be an asymmetric key when using RS2561

when running remotely, but the code does work consistently when running locally.

Edit 12/22/2023:

I created this SO post on the above error.

@dlcole
Copy link
Author

dlcole commented Jan 5, 2024

I was ultimately able to get this working by switching to Firebase Hosting to deploy the server code that invokes the HTTP v1 interface. I still hope to create a GitHub repository with the working code once I get it cleaned up.

@dlcole
Copy link
Author

dlcole commented Jan 6, 2024

I have created the httpV1Msg repository to serve as a sample implementation for using the Firebase HTTP v1 API via a Node.js app on Firebase Hosting.

@dlcole
Copy link
Author

dlcole commented Jan 6, 2024

I'm closing this issue as my migration is now complete.

@dlcole dlcole closed this as completed Jan 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant