I’m not sure if the OP ever got this going, so this is for future readers. It is quite lengthy, but I was able to get this working utilizing a webserver and Node js. A little background, I mainly need notifications for player-to-player turn-based notifications, so while this approach works for that and should still work for sending mass notifications, there are certainly better ways to accomplish mass notifications than this one. I am also not a Node js person, so I am sure there is a better way to do this as well! Most of it came from online sources.
Main steps:
- Create a js script that runs every 45 minutes on the webserver that trades your google credentials for an oauth2 authorization token that is then stored in a file on the webserver.
- Setup a listening Node js service that will authenticate client requests via (API_KEY/API_SECRET) and allow the download of the file from above.
- App retrieves this file every time it needs to send a push notification.
I store the entire project and Node js itself in a folder called /srv/googleAuth on the webserver. (I am using Debian 12).
First, create a “.env” file in the project folder that contains nothing but your made-up api key and secret. No spaces or anything else. Much documentation on the web concerning “.env” files.
API_KEY=YOUR_API_KEY
API_SECRET=YOUR_API_SECRET
Next, create a Node js script to make the request to Google for the authorization token. I call this script “reqAuth.js”. The oauth2 token expires in about an hour, so run this script every 45 minutes or so (cron) to keep it viable.
// this script will get an oauth2 token from Google and store this token in a file on the webserver.
const { GoogleAuth } = require('google-auth-library');
const fs = require('fs');
const filePath = '/srv/googleAuth/secure/authToken.txt'; // path to where the google token will be stored
async function getAccessToken() {
const scopes = ['https://www.googleapis.com/auth/firebase.messaging'];
const keyFilePath = '/srv/googleAuth/serviceAccountKeys.json'; // location of your stored google credentials for firebase.
const auth = new GoogleAuth({
keyFile: keyFilePath,
scopes: scopes
});
try {
const client = await auth.getClient();
const accessToken = await client.getAccessToken();
// console.log('Access Token:', accessToken.token);
fs.writeFile(filePath, accessToken.token, (err) => {
if (err) {
console.error('Error writing file:', err);
} else {
// console.log('Variable successfully stored in file and overwritten.');
}
});
return accessToken.token;
} catch (error) {
console.error('Authentication error:', error);
}
}
getAccessToken();
Now that we have the oauth2 token refreshing every 45 minutes, let’s setup a Node js service that listens for incoming requests from clients who are seeking the oauth2 token. I call this script “authServer.js” and it is loaded on bootup.
require('/srv/googleAuth/node_modules/dotenv').config({ path: '/srv/googleAuth/.env' });
const https = require('https');
// Node.js file system module
const fs = require('fs');
const path = require('path');
const sslOptions = {
key: fs.readFileSync('/etc/letsencrypt/live/<MY_WEBSITE>/privkey.pem'),
// Important, use your full chain next and not your intermediate.
cert: fs.readFileSync('/etc/letsencrypt/live/<MY_WEBSITE>/fullchain.pem')
};
const API_KEY = process.env.API_KEY;
const API_SECRET = process.env.API_SECRET;
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// middleware to validate api key
function authenticateApi(req, res, next) {
const apiKey = req.headers['x-api-key'];
const apiSecret = req.headers['x-api-secret'];
// Check if both key and secret are provided and match
if (apiKey && apiSecret && apiKey === API_KEY && apiSecret === API_SECRET) {
next(); // Credentials are valid, proceed to the next handler
} else {
// Authentication failed
res.status(401).send('Unauthorized: Invalid API credentials');
}
}
// Apply the authentication middleware to a specific route or globally
app.use(authenticateApi);
// I use "/srv/googleAuth/secure" to store the authenticated google token, so it needs to be secured.
app.get('/secure', (req, res) => {
const secretInfo = `The secret is: ${process.env.API_SECRET}`;
res.json({
message: 'This is secure data, only accessible with a valid API key.',
info: secretInfo
});
});
// Now secure endpoint to request a specific file. This is the file that contains the token.
app.get('/secure/:filename', (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'secure', filename);
// Security checks:
// 1. Check if the file exists
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found.');
}
// 2. Ensure the file requested is within the designated secure directory
// This prevents "path traversal" attacks
if (!filePath.startsWith(path.join(__dirname, 'secure'))) {
return res.status(403).send('Forbidden: Access denied to this file path.');
}
// Use express's built-in download method to send the file securely
res.download(filePath, (err) => {
if (err) {
console.error('File download error:', err);
res.status(500).send('Could not download the file.');
}
});
});
// Create the HTTPS server
const server = https.createServer(sslOptions, app);
server.listen(PORT, () => {
console.log(`HTTPS Server running on https://localhost:${PORT}`);
});
Finally, in my solar2d app I download the token from my webserver first, then pair that token with my push request.
local json = require("json")
local network = require("network")
local ProjId = "<your_google_project_id>"
local OAUTH_API_KEY = "<YOUR_API_KEY>"
local OAUTH_API_SECRET = "<YOUR_API_SECRET>"
local SERVER_URL = "https://<your web address>:3000/secure/authToken.txt"
local deviceToken="rtgQX7qRTjyorQa19PsFPR:APA91bFdOWhNQbPbJyLNCbIdv1OFMra15hPky0mQ53yYbXMdQQjyOhx45BJYHEuwykaxt2dZaRtkvheAHco6RUJ_Ht-oRoZggInAfb8UoUxiMUPmYn4mQgk"
-- function to listen for response and send the push if successful
local function networkListener(event)
if event.isError then
print("Network error: " .. event.status)
else
-- oauth token retrieval was successful so send push notification
if event.status == 200 then
oauthToken=event.response
local title = "Your Notification Title âšľ"
local alert = "Your notification message!"
local sound = "notification.mp3"
local pushUrl = "https://fcm.googleapis.com/v1/projects/"..ProjId.."/messages:send"
local headers = {}
local body = {}
local FCMkey = "Bearer "..oauthToken
headers =
{
["Content-Type"] = "application/json",
["Authorization"] = FCMkey
}
body =
{
["message"] =
{
["token"] = deviceToken,
["notification"] =
{
["title"] = title,
["body"] = alert,
},
["android"] =
{
["notification"] =
{
["sound"] = sound,
}
},
["apns"] =
{
["payload"] =
{
["aps"] =
{
["sound"] = sound,
}
}
}
}
}
local function pushResponse(event)
-- do stuff with responses
end
local params = {}
params.headers = headers
params.body = json.prettify(body)
network.request( pushUrl, "POST", pushResponse, params )
else
print("Server error: " .. event.status)
end
end
end
-- get oauth2 token from web server first
local params = {
headers =
{
["Content-Type"] = "application/json",
["x-api-key"] = OAUTH_API_KEY,
["x-api-secret"] = OAUTH_API_SECRET
}
}
network.request(
SERVER_URL,
"GET",
networkListener,
params
)