We have a cross-platform app that uses Firebase Cloud Messaging to drive an in-app chat feature. Some users might use the app actively on more than one device. So, whenever a user's device receives an onTokenRefresh trigger, we send that new registration token to the server to be saved against the user. Now say a user already has some registration tokens stored in the server database, how will we know if those tokens were for the same device and should now be deleted or if they are for a different device and we should keep sending to all of them?
I have read the docs on Device Group Messaging, but it looks like too much overhead for our application and it doesn't look like the Firebase server will automatically delete a superseded registration token from the group for you.
If we simply assume all the user's registration tokens on record are active and send to all, can we use the response to decide if we need to prune a token on the server?
{
"multicast_id": 6538766984100364080,
"success": 1,
"failure": 0,
"canonical_ids": 0,
"results": [
{
"message_id": "0:1510294979553090%029da28f029da28f"
}
]
}
According to this answer and some tests against the HTTP API with replaced tokens, it doesn't look like the "success":1 result is a reliable indicator that the token should not be removed, because replaced tokens tend to live on. Also, a "success": 0 result might not be a reliable indicator that we can remove the token, because it might just indicate an ad-hoc network error on a valid, active token.
The API documentation talks about how to interpret an optional registration_id in the result, but it is not clear how this differs from a NotRegistered error and what the best action is to take.
Any insight or best practice on how to handle and manage the arrival of a FCM device token on the server will be much appreciated.
I also came across the exact challenge and had to resolve to a solution:
Storing each token for the user against the device id.
It's interesting enough to know that this function in fact exists in the firebase messaging method. But more surprising is the fact that there's no documentation to handle such scenario.
https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId.html#getId()
So in summary, while sending the new token to the server, also send along the device id returned by the getId() method and use it to enforce uniqueness of token per device.
Problem solved :D
We are going with the approach where we assume all onTokenRefresh ids are new, additional devices that we add to the device list on the server. Then, whenever we send a message we use the returned result to delete or replace deprecated device tokens. Implementation in PHP:
// $devices is a list of the device ids to send to
// 1. send a message to a list of devices
$response = Firebase::request('POST', 'send', ['json' => $this->payloadFor($devices)]);
// 2. check the response to see if we need to make changes to the device list
// if it is a network error, no changes needed
if ($response->getStatusCode() != 200) {
Log::info("FCM http error " . $response->getStatusCode());
return;
}
$body = json_decode($response->getBody(), $asArray = true);
// do we need to dig deeper?
if ($body['failure'] == 0 && $body['canonical_ids'] == 0) return;
if (count($body['results']) != count($devices)) {
Log::info("FCM error : device count not matching result count");
return;
}
// we have errors that need processing, so step through the results list
foreach ($body['results'] as $key => $result) {
if (isset($result['error'])) {
switch ($result['error']) {
case 'NotRegistered':
case 'InvalidRegistration':
$deletedRows = Device::where('token', $devices[$key])->delete();
Log::info("FCM trimmed: $devices[$key]");
break;
default:
Log::info("FCM error " . $result['error']);
break;
}
}
// we need to update some device tokens
if (isset($result['registration_id'])) {
Device::deprecate($devices[$key], $result['registration_id']);
Log::info("FCM replaced: " . $devices[$key]);
}
}
Related
I am calling a callable cloud function from a Javascript frontend and when calling Firebase in the past I would chain firebase.auth().currentUser.getIdToken(... before my call to the backend to ensure I had a valid token. Now that I am switching to callable cloud functions, I am wondering if this token refresh check is embedded in the callable itself or if I have to still check that my token is valid.
When calling a method returned by the callable builder, like const myFunc = httpsCallable(funcName); myFunc(/* data */);, only the current ID token is attached. If the token has not yet expired, it is not forcibly refreshed.
At least for the JavaScript SDK, this is seen in the source code of packages/functions/src/service.ts and packages/functions/src/context.ts:
// in packages/functions/src/service.ts
const context = await functionsInstance.contextProvider.getContext();
if (context.authToken) {
headers['Authorization'] = 'Bearer ' + context.authToken;
}
// in packages/functions/src/context.ts
async getAuthToken(): Promise<string | undefined> {
if (!this.auth) {
return undefined;
}
try {
const token = await this.auth.getToken();
return token?.accessToken;
} catch (e) {
// If there's any error when trying to get the auth token, leave it off.
return undefined;
}
}
This essentially leads to the following decisions:
If Firebase Authentication isn't loaded, return undefined (don't attach any tokens).
If the no one is logged in, return null (don't attach any tokens).
If the token has expired, attempt to get a fresh one (and then attach it to the request).
If the token can't be obtained, return undefined (don't attach any tokens).
If the token has not expired, return the access token (and then attach it to the request).
Even though token expiry is handled by the SDK, you can still forcibly freshen up the token by using getIdToken(/* forciblyRefresh: */ true) before calling the function. The Cloud Functions SDK will call the Admin SDK to verify whatever token is sent as soon as the request is received regardless.
Aside from that, you can further enhance the security of your Cloud Function by enforcing a cutoff on how long ago the user signed into their account for privileged actions like account deletion, changing service account details and so on. This is done using the auth_time claim inside the access token's data or the authTime property on the id token object.
I am using REST API in my app to communicate with a Firebase RTDB, and trying to use a Google Access Token to authenticate my requests.
My issue is that with even the most permissive Rules on the database, I get HTTP error 401 in response to queries that try to authenticate.
For example, say I try to put some data in my database with the following command, I get 401 in return (all the values within < > are placeholders):
curl -XPUT -d '{ "UserID" : "<GOOGLE_UID>", "UserName" : "Clicksurfer", "CompletionMoves" : 8, "CompletionTime" : 16.21979 }' https://<FIREBASE_URL>.firebaseio.com/Level2/<GOOGLE_UID>.json/?access_token=<GOOGLE_ACCESS_TOKEN>
401
The strangest part is, when I abandon the use of access token altogether the query works:
curl -XPUT -d '{ "UserID" : "<GOOGLE_UID>", "UserName" : "Clicksurfer", "CompletionMoves" : 8, "CompletionTime" : 16.21979 }' https://<FIREBASE_URL>.firebaseio.com/Level2/<GOOGLE_UID>.json
200
As I said, I am currently using the most permissive rules for debugging:
{
"rules": {
".read": true,
".write": true
}
}
Any idea what might be causing this? Thanks in advance
EDIT:
I use the Google Play Games plugin for Unity in my project, among other things to get the AuthCode.
In order to do this, I needed to do a couple of things:
When building the config for Google Play Games during startup, I made sure to call the RequestServerAuthCode(false) method
Have the user login after Google Play Games sets up
Make sure that the relevant ClientID was supplied to Unity (in this case, it is a web client that has auth permissions on my Firebase rtdb).
This all looks like this:
public class GPGSAuthentication : MonoBehaviour
{
public static PlayGamesPlatform platform;
void Start()
{
if (platform == null)
{
PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder().RequestServerAuthCode(false).Build();
PlayGamesPlatform.InitializeInstance(config);
PlayGamesPlatform.DebugLogEnabled = true;
platform = PlayGamesPlatform.Activate();
}
Social.Active.localUser.Authenticate(success =>
{
if (success)
{
Debug.Log("GSPS - Logged in successfully");
}
else
{
Debug.Log("GSPS - Falied to login");
}
});
}
}
Now that we've done this, we can call PlayGamesPlatform.Instance.GetServerAuthCode() in order to get the AuthCode.
I traded in my AuthCode for an Access Token by sending a POST request to https://www.googleapis.com/oauth2/v4/token. In my query, I supply 4 fields:
client_id, which has the ID of the previously used client (where we got the AuthCode from).
client_secret, which has the correlating secret.
grant_type, which is always with the value "authorization_code"
code, which has the value of the AuthCode we got.
In response, I get a 200 response with 4 parameters:
access_token, the token I (fail to) use when authenticating against my Firebase rtdb.
token_type, the type of the aforementioned token.
expires_in, the amount of time before the token expires (I presume in seconds unit)
refresh_token, a token which can be used in order to get a new access_token without having to keep the Google user connected.
I then supply this access_token value to the queries I send to my DB, and promptly get the 401 error.
I'm trying to connect Google Home to an external chatbot with actionssdk. I have an API that take user inputs and send them to my chatbot with webhook, but my chatbot make a response calling another endpoint of my API in an async way, and I can't show the response in actions on Google or Google Home.
I create an actionssdkApp.
const {
actionssdk,
SimpleResponse,
Image,
} = require('actions-on-google');
var app = actionssdk();
var express_app = express();
My API has 2 endpoints. One of them is for actions on google to send user inputs to my chatbot:
app.intent('actions.intent.MAIN', conv => {
console.log('entra en main');
conv.ask('Hi, how is it going?');
});
app.intent('actions.intent.TEXT', (conv, input) => {
var userId = conv.body.user.userId;
console.log(userId);
if(userId && input){
textFound(conv, input, userId);
}else{
textnotFound(conv);
}
});
TextFound function send user inputs to my chatbot with webhook, but the request doesn't receive the response. My chatbot call another endpoint with the text answer:
express_app.post('/webhook', bodyParser.json(), (req, res)=>{
console.log("Webhook");
const userId = req.body.userId;
if (!userId) {
return res.status(400).send('Missing User ID');
}
console.log(req.body);
res.sendStatus(200);
});
And here is where I want to send the answer to Google Home. But I need the conv object to show the answer in google Home, or actions on google, or any other device.
Edit:
My textFound function:
webhook.messageToBot(metadata.channelUrl, metadata.channelSecretKey, userId, input, function(err){
if(err){
console.log('Error in sending message');
conv.ask("Error in sending message");
}else{
conv.ask("some text");
}
});
From here my api send user inputs to my bot through messageToBot function:
request.post({
uri: channelUrl,
headers: headers,
body: body,
timeout: 60000,
followAllRedirects: true,
followOriginalHttpMethod: true,
callback: function(err, res, body) {
if (err) {
console.log('err: '+err);
callback(err);
} else {
console.log('Message sent');
callback(null);
}
}
});
From now on, my bot doesn't send a response but makes a call to /webhook endpoint of my api with the answer. But in this function I haven't de conv object and I can't send the answer to google. I don't know how to access to this object. Maybe there is an uri to connect with my project in actions on google from my api.
Typically, Actions on Google works in a request-response way. The user says something to the Action, and the Action replies with a response. That reply needs to come within about 5 seconds. If you think the call to /webhook can come that quickly, and you will only deliver a message to the user after they say something, you can have /webhook save the response in a queue for the user, and have your Intent handler be in a loop that checks this queue for any messages to reply with - if there is a message within 5 seconds, you reply with it, if not, you need to reply before the 5 seconds are up.
If you can't guarantee it will be done within 5 seconds, however, there are a couple of workarounds that might be useful depending on your needs.
The first is that you might be able to use notifications. In this scenario, you would send the message from the user and then close the conversation. When your /webhook endpiont is triggered, you would locate the user and send the notification to their Assistant. Unfortunately, this is a bit bulky, doesn't lead to a very interactive chat system, and notifications also aren't supported on smart speakers.
You can also look into using a Media Response to set up a way for you to poll for new messages periodically. Under this scheme, your user would send their message. In your reply to them, you would include a Media Response for some audio that plays for, say, 15 seconds. When the audio finishes, your Action will be called again and you can check to see if any messages have been queued up to be delivered to the user. If so, you relay those messages, followed by a Media Response gain. Otherwise, just send a Media Response. Your call to /webhook would have to put messages in a queue to be delivered to the user. This is more complex, especially to scale, but can be made more interactive. It is also a more general case of trying to handle it in a loop inside 5 seconds.
I am thinking about keeping all registration ids(push token) in DB and sending notifications to user from iPhone. I tried something like this but did not get any notification.
func sendPNMessage() {
FIRMessaging.messaging().sendMessage(
["body": "hey"],
to: TOKEN_ID,
withMessageID: "1",
timeToLive: 108)
}
What I am doing wrong or maybe it is impossible at all?
Currently it's not possible to send messages from the application itself.
You can send messages from the Firebase Web Console, or from a custom server using the server-side APIs.
What you might want to do is to contact a server (like via http call) and that server will send the message to the user.
This way ensure that the API-KEY of the server is protected.
PS: the sendMessage(..) api is called upstream feature, and can be used to send messages from your app to your server, if you server has an XMPP connection with the FCM server.
Yes you can send push notification through Firebase.Please make sure do NOT include the server-key into your client. There are ways "for not so great people" to find it and do stuff... The Proper way to achieve that is for your client to instruct your app-server to send the notification.
You have to send a HTTP-Post to the Google-API-Endpoint.
You need the following headers:
Content-Type: application/json
Authorization: key={your_server_key}
You can obtain your server key within in the Firebase-Project.
HTTP-Post-Content: Sample
{
"notification": {
"title": "Notification Title",
"text": "The Text of the notification."
},
"project_id": "<your firebase-project-id",
"to":"the specific client-device-id"
}
Google Cloud Functions make it now possible send push notifications from device-to-device without an app server.
From the Google Cloud Functions documentation:
Developers can use Cloud Functions to keep users engaged and up to
date with relevant information about an app. Consider, for example, an
app that allows users to follow one another's activities in the app.
In such an app, a function triggered by Realtime Database writes to
store new followers could create Firebase Cloud Messaging (FCM)
notifications to let the appropriate users know that they have gained
new followers.
Example:
The function triggers on writes to the Realtime Database path where followers are stored.
The function composes a message to send via FCM.
FCM sends the notification message to the user's device.
Here is a demo project for sending device-to-device push notifications with Firebase and Google Cloud Functions.
Diego's answer is very accurate but there's also cloud functions from firebase it's very convenient to send notifications in every change in the db. For example let's say you're building chat application and sending notification in every new follower change.
This function sample is very good example.
For more information about cloud functions you can check official docs.
I have an app that has a "send feedback to developer" section. I also have a User collection in my firestore database. When a user logs into the app, I have that Users data update their FCM token with the following code in my SceneDelegate.swift:
import Firebase
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
authListener = Auth.auth().addStateDidChangeListener({ (auth, user) in
Auth.auth().removeStateDidChangeListener(self.authListener!)
if user != nil {
DispatchQueue.main.async {
let docRef = Firestore.firestore().collection("User").document((user?.email)!)
docRef.getDocument { (snapshot, error) in
guard let snapshot = snapshot else {return}
Messaging.messaging().token { token, error in
if let error = error {
print("Error fetching FCM registration token: \(error)")
} else if let token = token {
docRef.updateData(["FCMtoken":token])
print("FCM registration token: \(token)")
}
}
}
}
}
})
guard let _ = (scene as? UIWindowScene) else { return }
}
then in my feedback view controller i have this code to send my specific device (but you can look up/fetch which specific device you want in your database where the FCMtoken is stored where i have INSERT-DEVICE-TOKEN-HERE). The url to send to is "https://fcm.googleapis.com/fcm/send" and you can find YOUR-APP-FCM-KEY by going to your project settings in firebase, going to cloud messaging tab and its the server key.
func sendMePushNotification() {
let token = "INSERT-DEVICE-TOKEN-HERE"
if let url = URL(string: "https://fcm.googleapis.com/fcm/send") {
var request = URLRequest(url: url)
request.allHTTPHeaderFields = ["Content-Type":"application/json", "Authorization":"key=YOUR-APP-FCM-KEY"]
request.httpMethod = "POST"
request.httpBody = "{\"to\":\"\(token)\",\"notification\":{\"title\":\"Feedback Sent!\",\"body\":\"\(self.feedbackBox.text!)\",\"sound\":\"default\",\"badge\":\"1\"},\"data\": {\"customDataKey\": \"customDataValue\"}}".data(using: .utf8)
URLSession.shared.dataTask(with: request) { (data, urlresponse, error) in
if error != nil {
print("error")
} else {
print("Successfully sent!.....")
}
}.resume()
}
}
Use onesignal,you can send device to notifications or device to segments ,it can work with firebase in this way
Use onesignal functions to create a specific id,save it in a firebase database ,then when the id can be put in another function that is used to send a notification
Notes: 1-i am using it in my apps with firebase works perfectly
2-i can submit that code,just someone comments so i can find this answer
I am at the intermediate level in php and am new with facebook development. I have looked through the facebook documents and Stack Overflow previous comments.
All I basically wanted to do was let the user log in with their Facebook account and display their name.
My php page has a graph, and the page auto refreshes every 2 or 5 min.
I authenticate and get the facebook first_name to put on the page.
$graph = $facebook->api('/me');
echo $graph['first_name'] to get the first name of the user .. (for which I thought that no access token was required).
After about 90 min. I have been receiving the error:
fatal error: Uncaught OAuthException: An active access token must be used to query information about the current user......
and I have no value ( 0 ), in the $facebook->getUser(); parameter
I do know that off line access permission has been depreciated, (and I have have this enabled in my apps advanced settings)
I am trying to get an extended access token. In the FB docs. I see:
https://graph.facebook.com/oauth/access_token?
client_id=APP_ID&
client_secret=APP_SECRET&
grant_type=fb_exchange_token&
fb_exchange_token=EXISTING_ACCESS_TOKEN
I included my information in the link(an existing valid access token and all) and received a access token:
access_token=AAADbZBPuUyWwBAFubPaK9E6CnNsPfNYBjQ9OZC63ZBN2Ml9TCu9BYz89frzUF2EnLttuZAcG2fWZAHbWozrvop9bQjQclxVYle7igvoZCYUAg2KNQLMgNP&expires=4050
Yet this token expired in about 1 hour or so.(....expires=4050)
I assume I am using server side auth because I am using PHP?
I assume you need to enable "deprecate offline_access" in your Apps Advanced Settings page. As this worked for me:
//added code in base_facebook.php inside the facebook class
public function getExtendedAccessToken(){
try {
// need to circumvent json_decode by calling _oauthRequest
// directly, since response isn't JSON format.
$access_token_response =
$this->_oauthRequest(
$this->getUrl('graph', '/oauth/access_token'),
$params = array( 'client_id' => $this->getAppId(),
'client_secret' => $this->getAppSecret(),
'grant_type'=>'fb_exchange_token',
'fb_exchange_token'=>$this->getAccessToken(),
));
} catch (FacebookApiException $e) {
// most likely that user very recently revoked authorization.
// In any event, we don't have an access token, so say so.
return false;
}
if (empty($access_token_response)) {
return false;
}
$response_params = array();
parse_str($access_token_response, $response_params);
if (!isset($response_params['access_token'])) {
return false;
}
return $response_params['access_token'];
}
The token can still be invalid for several reasons, See How-To: Handle expired access tokens.
Hope it helps
There's a bug on this:
https://developers.facebook.com/bugs/241373692605971
But, another question on SO has a workaround (user uninstalls and re-installs):
fb_exchange_token for PHP only working once user removes app