{"reason":"BadDeviceToken"} http2 IOS notifications from Nodejs - ios

I am trying to send push notifications using http2 api from my node backend.
I have the following with me from the IOS team .
.p8 AuthKey
Team ID
Key ID
We have generated the build from the production environment.
Key is generated using the AppStore selection.
I dont see any environment mismatch in the key, Device token and the build.
But still I get
:status: 400 apns-id: 91XXXX-XXXX-XXXX-XXXX-3E8XXXXXX7EC
{"reason":"BadDeviceToken"}
Code from Node backend :
const jwt = require('jsonwebtoken');
const http2 = require('http2');
const fs = require('fs');
const key = fs.readFileSync(__dirname + "/AuthKey_XXXXXXXXXX.p8", 'utf8')
const unix_epoch = Math.round(new Date().getTime() / 1000);
const token = jwt.sign(
{
iss: "XXXXXXXXXX", //"team ID" of developer account
iat: unix_epoch
},
key,
{
header: {
alg: "ES256",
kid: "XXXXXXXXXX", //issuer key "key ID" of p8 file
}
}
)
const host = 'https://api.push.apple.com'
const path = '/3/device/<device_token>'
const client = http2.connect(host);
client.on('error', (err) => console.error(err));
const body = {
"aps": {
"alert": "hello",
"content-available": 1
}
}
const headers = {
':method': 'POST',
'apns-topic': 'com.xxxxxx.xxxxxx', //your application bundle ID
':scheme': 'https',
':path': path,
'authorization': `bearer ${token}`
}
const request = client.request(headers);
// request.on('response', response => {
// console.log("apnresponse",JSON.stringify(response));
// });
request.on('response', (headers, flags) => {
for (const name in headers) {
console.log(`${name}: ${headers[name]}`);
}
});
request.setEncoding('utf8');
let data = ''
request.on('data', (chunk) => { data += chunk; });
request.write(JSON.stringify(body))
request.on('end', () => {
console.log(`\n${data}`);
client.close();
});
request.end();
IOS team is able to successfully send notifications to the device using the firebase console.
PUsh notifications fail only when I try from the node backend.
According to the Apple documentation, neither the device token is invalid, nor I am using production certificate for the development server or vice versa;
neither of which are the case here.
How can I make this work?

Related

How to transition from an APNS development to production provider?

The API is written in NodeJS
Currently,
The app is capable of send a push notification in Apple's development environment using the code below.
Getting a BadDeviceToken 400 Error
Also, note development notifications were working (SO will not format my code below):
module.exports.sendNotification = (deviceToken, msg, payload) => {
const jwt = require("jsonwebtoken");
const http2 = require("http2");
const fs = require("fs");
const key = fs.readFileSync(process.env.APNS_KEY, "utf8");
// "iat" should not be older than 1 hr from current time or will get rejected
const token = jwt.sign(
{
iss: process.env.APNS_TEAM_ID, // "team ID" of your developer account
iat: Date.now() / 1000 // Replace with current unix epoch time
},
key,
{
header: {
alg: "ES256",
kid: process.env.APNS_KEY_ID // issuer key which is "key ID" of your p8 file
}
}
);
/*
Use 'https://api.production.push.apple.com' for production build
*/
const host = process.env.APNS_HOST;
const path = `/3/device/${deviceToken}`;
const client = http2.connect(host);
client.on("error", (err) => console.error(err));
const body = {
aps: {
alert: msg,
"content-available": 1,
payload
}
};
const headers = {
":method": "POST",
"apns-topic": process.env.APNS_TOPIC, // your application bundle ID
":scheme": "https",
":path": path,
authorization: `bearer ${token}`
};
const request = client.request(headers);
request.on("response", (headers, flags) => {
for (const name in headers) {
console.log(`${name}: ${headers[name]}`);
}
return {
headers
}
});
request.setEncoding("utf8");
let data = "";
request.on("data", (chunk) => { data += chunk; });
request.write(JSON.stringify(body));
request.on("end", () => {
console.log(`\n${data}`);
client.close();
});
request.end();
};
The desired outcome is to send a push notification using Apple's production APNS environment. My best try at solving this has been swapping the development url for the production, that returns a, "{"reason":"BadDeviceToken"} :status: 400 apns-id: "
Tries so far...
Here are the hosts I'm using:
Production = "https://api.push.apple.com" also tried "https://api.push.apple.com:443"
Development = "https://api.sandbox.push.apple.com"
Other things I've tried:
Certificates instead of tokens; not sure if I'm doing it right. So if you know, please drop the code it.
ChatGPT's sol'n:
const apn = require('apn');
// Path to the certificate file and passphrase (if any) const cert = '/path/to/cert.pem'; const key = '/path/to/key.pem'; const passphrase = 'your_certificate_passphrase';
// Create the APN provider with the certificate and key const provider = new apn.Provider({ cert: cert, key: key, passphrase: passphrase, production: true // set to false for development environment });
// Create the notification payload const payload = new apn.Payload({ alert: 'Hello World!', sound: 'default', badge: 1 });
// Send the notification to a device token const deviceToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; const note = new apn.Notification(); note.expiry = Math.floor(Date.now() / 1000) + 3600; note.payload = payload; note.topic = 'com.example.myapp'; provider.send(note, deviceToken).then(result => { console.log(result); });
I'm not sure where to find the passphrase for above, so I just removed it. The key, I found in the .pem file and just cut and pasted it out of that from the key beginning and end and replaced the old .p8 file text with it.
Also tried, SO answers: How do I switch the certificate from development to production?
Also tried, SO answers: iOS push notification device token for development and production
Also tried, iOS APNS Development [sandbox] vs Production
Also tried, changing the environment under the 'APS Environment' key in the Entitlements File from 'development' to 'production' and then generated a new device token.
When transitioning your APNS API/Backend Side Provider you need to construct the Provider like so...
const fs = require("fs");
const apn = require('apn');
// Create the APN provider with the certificate and key
const key = fs.readFileSync(process.env.APNS_KEY, "utf8");
const cert = fs.readFileSync(process.env.APNS_CERT, "utf8");
const provider = new apn.Provider({
token: {
cert: cert,
key: key,
teamId: process.env.APNS_TEAM_ID,
keyId: process.env.APNS_KEY_ID
},
production: true // set to false for development environment
});
Building the Provider, Cert and Key Look
Note that I'm using the cert from Developer portal AND the key to sign it. This is in the same section of the Apple Developer portal as the Development token (eg the .p8 file). These files (.pem and .p8) begin and end with, "-----BEGIN CERTIFICATE-----
", "-----END CERTIFICATE-----", "-----BEGIN PRIVATE KEY-----
", "-----END PRIVATE KEY-----". You will often see "token" in the instructions online, that is the object you pass into the Provider constructor as an arg. The token includes the cert and key, along with the other fields you see in the code.
NEXT, this took days for me to figure out. You cannot force the Simulator or side loading to create a production Device Token by changing the Entitlements File in XCode to, 'production' instead of 'development'. You MUST (as in there were no Push Notifications making it to my side loaded app through my API to APNS to my device, and I was getting "BadDeviceToken" instead) at least install the TestFlight version of your app in order to get a production Device Token. Once you have that you may pass it to your API, then use it send APNs to device(s).
Init Provider Options
You may also init your Provider like above instead of the "jwt" method. I've tried both and the above way seems the easiest and cleanest (less code).
WATCH OUT for tutorials that claim there is a way to determine between these two tokens, with "
const firstByte = parseInt(deviceToken.substr(0, 2), 16);
// Successfully determines the aps environment
const apsEnvironment = (firstByte & 0x80) ? 'production' : 'development';
"
Notification Content and Delivery
The following goes below the code snippet above...
const notification = new apn.Notification();
notification.alert = {
title: 'Hello World',
body: 'This is a test notification'
};
notification.topic = process.env.APNS_TOPIC;
console.log(`notification ${JSON.stringify(notification)}`)
provider.send(notification, deviceToken).then((result) => {
console.log(JSON.stringify(result));
});
This seems self exclamatory and did not hold me up. Also, there if you go to the definition of Notification, there is a payload attribute which you may define as other tutorials mentions.
Cheers! Happy Hacking!

Salesforce Authentication in NodeJs

I am trying to authenticate Salesforce to NodeJS application.I getting error Like Error: invalid_grant - authentication failure .What else I am missing here. Do I need to do any configurations from salesforce side. Here is my code. Could Someone help me on this?
app.js
var nforce = require('nforce');
const client_id = '**'
const client_secret = '****'
const redirect_uri = 'https://***.sandbox.my.salesforce.com/services/oauth2/success'
const sfdc_user = '*****'
const sfdc_pass = '***'
const credentials = {
client_id :client_id,
client_secret:client_secret,
grant_type:"password",
username:sfdc_user,
password:sfdc_pass
}
async function getConnection(){
const loginUrl = "https://***.sandbox.my.salesforce.com/services/oauth2/token";
var org = nforce.createConnection({
clientId: credentials.client_id,
clientSecret: credentials.client_secret,
redirectUri: redirect_uri,
});
console.log('org >>'+JSON.stringify(org));
let oauth= await org.authenticate({ username: credentials.username, password: credentials.password});
console.log('oauth >>'+oauth); //Couldnt get this console
const access_token = oauth.access_token;
const sf_auth_url = oauth.instance_url + '/services/data/v48.0/'
sf_auth = {
'Authorization':'Bearer ' + access_token,
'Content-type': 'application/json',
'Accept-Encoding': 'gzip'
}
return { sf_auth,sf_auth_url }
}
module.exports = { getConnection }
main.js
const f = require('./app');
const https = require('https')
const fs = require('fs')
const port = 3000
const server = https.createServer(function(req,res){
res.writeHead(200,{'Content-Type': 'text/html'})
res.end();
})
server.listen(port,function(error){
if(error){
console.log('Something Went Wrong!')
}else{
console.log('Server is listening on port '+port)
f.getConnection();
}
})
Usually when I have received this error it's due to not having the user's security security token appended to the end of the password field. This is the token sent to you via email when you first set your password or performed your last password reset. If you need to reset it you can do so via Personal Settings > Reset My Security Token
Reset Your Security Token
In the event it's not that, this username/password authentication documentation should help.
OAuth 2.0 Username-Password Flow for Special Scenarios

Flutter: how to fix callkeep is not showing incoming call screen when the app is in background in ios?

I just build a video call app in flutter for android and ios,
I used callkeep to show incoming call notification and fcm push notification.
In ios it works when the app is in foreground. but it is not showing when the app is in background.
In android it works both in foreground and background.
How to fix this issue?
you should use this package for your case flutter_ios_voip_kit, this is the link: https://pub.dev/packages/flutter_ios_voip_kit
For us the only reliable way to be "called" on iOS was using VoIP. You can use this also with callkeep. However you will need to call the APN not via firebase, but have to implement the call yourself.
In our case this looks like below. And there are several good tutorials on this. For example here: https://levelup.gitconnected.com/send-push-notification-through-apns-using-node-js-7427a01662a2
const key = fs.readFileSync(__dirname + "/AuthKey_XXXXXXXXXX.p8", 'utf8');
//"iat" should not be older than 1 hr from current time or will get rejected
const token = jwt.sign(
{
iss: "XXXXXXXXX", //"team ID" of your developer account
iat: Math.floor(new Date().getTime() / 1000)
},
key,
{
header: {
alg: "ES256",
kid: "XXXXXXXXXXX", //issuer key which is "key ID" of your p8 file
}
}
);
const options = {
':method': 'POST',
':scheme': 'https',
':path': '/3/device/' + deviceToken,
'apns-topic': 'XXX.ANEXAMPLE.ID.voip',//VERY IMPORTANT TO ADD THE .voip here
'apns-push-type': 'voip',
'apns-priority': '10',
'apns-expiration': '0',
'authorization': `bearer ${token}`
};
const uuid = crypto.randomUUID()
fullName = change.data().firstName + ' ' + change.data().lastName;
body = {
uuid: uuid,
caller_id: context.params.callerId,
caller_name: context.params.callerId,
has_video: true,
caller_id_type: "number"
};
strBody = JSON.stringify(body);
console.log("BODY: " + strBody);
let data = '';
const client = http2.connect('https://api.push.apple.com');
buff = Buffer.from(strBody);
req = client.request(options);
req.write(buff);
req.on('response', (headers) => {
for (const name in headers) {
console.log(`${name}: ${headers[name]}`)
}
})
.on('data', (chunk) => { data += chunk })
.on('end', () => {
console.log(`\n${data}`)
client.close()
})
.on('error', (err) => console.error(err));
req.end();

How to Update Device Configuration using Google Cloud functions and MQTT bridge

I am using the Google Cloud IoT with Pub/Sub.
I have a device reading sensor data and sending it to a topic in Pub/Sub.
I have a topic cloud function that is triggered by this message and I would like to have the device configuration updated, however I am unable to do so due to the following permission error.
index.js :
/**
* Triggered from a message on a Cloud Pub/Sub topic.
*
* #param {!Object} event The Cloud Functions event.
* #param {!Function} The callback function.
*/
var google = require('googleapis');
//var tt = google.urlshortener('v1');
//console.log(Object.getOwnPropertyNames(google.getAPIs()));
var cloudiot = google.cloudiot('v1');
function handleDeviceGet(authClient, name, device_id, err, data) {
if (err) {
console.log('Error with get device:', device_id);
console.log(err);
return;
}
console.log('Got device:', device_id);
console.log(data);
console.log(data.config);
var data2 = JSON.parse(
Buffer.from(data.config.binaryData, 'base64').toString());
console.log(data2);
data2.on = !data2.on;
console.log(data2);
var request2 = {
name: name,
resource: {
'versionToUpdate' : data.config.version,
'binaryData' : Buffer(JSON.stringify(data2)).toString('base64')
},
auth: authClient
};
console.log('request2' + request2);
var devices = cloudiot.projects.locations.registries.devices;
devices.modifyCloudToDeviceConfig(request2, (err, data) => {
if (err) {
console.log('Error patching device:', device_id);
console.log(err);
} else {
console.log('Patched device:', device_id);
console.log(data);
}
});
}
const handleAuth = (device_id) => {
console.log(device_id);
return (err, authClient) => {
const project_id = 'animated-bonsai-195009';
const cloud_region = 'us-central1';
const registry_id = 'reg1';
const name = `projects / ${project_id} /locations / ${cloud_region} /` +
`registries / ${registry_id} /devices / ${device_id}`;
if (err) {
console.log(err);
}
if (authClient.createScopedRequired &&
authClient.createScopedRequired()) {
authClient = authClient.createScoped(
['https://www.googleapis.com/auth/cloud-platforme']);
}
var request = {
name: name,
auth: authClient
};
// Get device version
var devices = cloudiot.projects.locations.registries.devices;
devices.get(request, (err, data) =>
handleDeviceGet(authClient, name, device_id, err, data));
}
};
exports.subscribe = (event, callback) => {
// The Cloud Pub/Sub Message object.
const pubsubMessage = event.data;
// We're just going to log the message to prove that
// it worked.
var obj = JSON.parse(Buffer.from(pubsubMessage.data, 'base64').toString());
console.log(Buffer.from(pubsubMessage.data, 'base64').toString());
console.log(event);
console.log(Object.getOwnPropertyNames(event));
console.log(callback);
let message = {
"watter": 1
};
message = new Buffer(JSON.stringify(message));
const req = {
name: event.data.deviceId,
resource: message
};
console.log(obj.deviceId);
google.auth.getApplicationDefault(handleAuth(obj['deviceId']));
// Don't forget to call the callback.
callback();
};
package.json :
{
"name": "sample-pubsub",
"version": "0.0.1",
"dependencies": {
"googleapis": "25.0.0"
}
}
Error:
A few options:
Check that you have enabled API access for the Google Cloud IoT Core API for the project used when creating the Google Cloud Function.
Check that you have enabled billing for your project
If you are deploying your Google Cloud Functions with gcloud beta functions deploy ... from the folder with your .js and package.json files, you may want to set the environment variables (GCLOUD_PROJECT and GOOGLE_APPLICATION_CREDENTIALS) or use gcloud auth application-default login before deploying in case you have multiple Google Cloud projects and need to enable the API on the configured one.
Update This community tutorial shows you how to do this - note that there have been some updates to Google Cloud Functions that require you to use a newer version of the Node JS client library as is done in the NodeJS sample and as corrected in this PR, note the version of the client library in package.json.

Why is OAuth2 with Gmail Nodejs Nodemailer producing "Username and Password not accepted" error

OAuth2 is producing "Username and Password not accepted" error when try to send email with Gmail+ Nodejs+Nodemailer
Code - Nodejs - Nodemailer and xoauth2
var nodemailer = require("nodemailer");
var generator = require('xoauth2').createXOAuth2Generator({
user: "", // Your gmail address.
clientId: "",
clientSecret: "",
refreshToken: "",
});
// listen for token updates
// you probably want to store these to a db
generator.on('token', function(token){
console.log('New token for %s: %s', token.user, token.accessToken);
});
// login
var smtpTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
xoauth2: generator
}
});
var mailOptions = {
to: "",
subject: 'Hello ', // Subject line
text: 'Hello world ', // plaintext body
html: '<b>Hello world </b>' // html body
};
smtpTransport.sendMail(mailOptions, function(error, info) {
if (error) {
console.log(error);
} else {
console.log('Message sent: ' + info.response);
}
smtpTransport.close();
});
issues:
I used Google OAuth2 playground to create the tokens, https://developers.google.com/oauthplayground/
It looks to grab a valid accessToken ok, using the refreshToken, (i.e. it prints the new access token on the screen.) No errors until it tries to send the email.
I added the optional accessToken: but got the same error. ( "Username and Password not accepted")
I am not 100% sure about the "username", the docs say it needs a "user" email address - I guess the email of the account that created to token, but is not 100% clear. I have tried several things and none worked.
I have searched the options on the gmail accounts, did not find anything that looks wrong.
Also, when I did this with Java, it needed the google userID rather than the email address, not sure why this is using the email address and the Java is using the UserId.
nodemailer fails with a "compose" scope
The problem was the "scope"
it fails with:
https://www.googleapis.com/auth/gmail.compose
but works ok if I use
https://mail.google.com/
Simply just do the following:
1- Get credentials.json file from here https://developers.google.com/gmail/api/quickstart/nodejs press enable the Gmail API and then choose Desktop app
2- Save this file somewhere along with your credentials file
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');
// If modifying these scopes, delete token.json.
const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';
// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
if(err){
return console.log('Error loading client secret file:', err);
}
// Authorize the client with credentials, then call the Gmail API.
authorize(JSON.parse(content), getAuth);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, (err, token) => {
if(err){
return getNewToken(oAuth2Client, callback);
}
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
* #param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback for the authorized client.
*/
function getNewToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
callback(oAuth2Client);
});
});
}
function getAuth(auth){
}
3 - Run this file by typing in your terminal: node THIS_FILE.js
4- You'll have token.json file
5- take user information from credentials.json and token.json and fill them in the following function
const nodemailer = require('nodemailer');
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;
const email = 'gmail email'
const clientId = ''
const clientSecret = ''
const refresh = ''
const oauth2Client = new OAuth2(
clientId,
clientSecret,
);
oauth2Client.setCredentials({
refresh_token: refresh
});
const newAccessToken = oauth2Client.getAccessToken()
let transporter = nodemailer.createTransport(
{
service: 'Gmail',
auth: {
type: 'OAuth2',
user: email,
clientId: clientId,
clientSecret: clientSecret,
refreshToken: refresh,
accessToken: newAccessToken
}
},
{
// default message fields
// sender info
from: 'Firstname Lastname <your gmail email>'
}
);
const mailOptions = {
from: email,
to: "",
subject: "Node.js Email with Secure OAuth",
generateTextFromHTML: true,
html: "<b>test</b>"
};
transporter.sendMail(mailOptions, (error, response) => {
error ? console.log(error) : console.log(response);
transporter.close();
});
If your problem is the scopes, here is some help to fix
Tried to add this as an edit to the top answer but it was rejected, don't really know why this is off topic?
See the note here: https://nodemailer.com/smtp/oauth2/#troubleshooting
How to modify the scopes
The scopes are baked into the authorization step when you get your first refresh_token. If you are generating your refresh token via code (for example using the Node.js sample) then the revised scope needs to be set when you request your authUrl.
For the Node.js sample you need to modify SCOPES:
// If modifying these scopes, delete token.json.
-const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
+const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
And then the call to oAuth2Client.generateAuthUrl will produce a url that will request authorization from the user to accept full access.
from the Node.js sample:
function getNewToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});

Resources