How to verify FCM registration token on server? - node.js

I got my Firebase Cloud Messaging registration token for web push.
And I sent this to my server to save in database for later push.
But how can I verify this token is valid or fake?
I have tried this but I think this is for Auth tokens not for web push.
Someone else can send request of a random fake token to my server. I want to prevent this before save in db.
Edit: It's solved and I wrote a simple class to use FCM for web push quickly.
https://github.com/emretekince/fcm-web-push

When sending to an invalid registration token, you'll should receive 200 + error:InvalidRegistration:
Check the format of the registration token you pass to the server. Make sure it matches the registration token the client app receives from registering with Firebase Notifications. Do not truncate or add additional characters.
This is the response when you try to send a simple cURL request where the registration token is just randomly made:
curl --header "Authorization: key=$[your_server_key_here]" \
--header Content-Type:"application/json" \
https://fcm.googleapis.com/fcm/send \
-d "{\"registration_ids\":[\"ABC\"]}"
Notice that I added in "ABC", in the registration_ids parameter. If ever it is a valid registration token, but is not associated to your project, you'll probably receive 200 + error:NotRegistered.
You can try sending a test message from your server to see the response without sending an actual message towards the device by using the dry_run parameter:
This parameter, when set to true, allows developers to test a request without actually sending a message.

Using Node Admin SDK
If anyone using firebase Admin SDK for node.js, there is no need to manually send the request using the server_key explicitly. Admin SDK provide sending dry_run push message to verify the fcm_token.
function verifyFCMToken (fcmToken) => {
return admin.messaging().send({
token: fcmToken
}, true)
}
Use this method like following
verifyFCMToken("YOUR_FCM_TOKEN_HERE")
.then(result => {
// YOUR TOKEN IS VALID
})
.catch(err => {
// YOUR TOKEN IS INVALID
})
Using Java Admin SDK
You can use following function
public Boolean isValidFCMToken(String fcmToken) {
Message message = Message.builder().setToken(fcmToken).build();
try {
FirebaseMessaging.getInstance().send(message);
return true;
} catch (FirebaseMessagingException fme) {
logger.error("Firebase token verification exception", fme);
return false;
}
}

One way is to send a message with the dry_run option = true, as is described by AL. in the other answer.
Another way is to use the InstanceId server API:
https://developers.google.com/instance-id/reference/server

According to the docs, you can use validate_only for testing the request without actually delivering the message.
https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send

You can refer to doc for using HTTP v1 of the firebase endpoint URL
After that, you could use this request body with "validate_only": true for testing the request without actually delivering the message.
{
"validate_only": true,
"message": {
"token": "your fcm token"
}
}

Related

Unauthenticated response status from firebase function

whenever i try to POST request to function https://europe-west3-[my-project].cloudfunctions.net/[my-function-name] i got a 401 response with body:
{
"error": {
"message": "Unauthenticated",
"status": "UNAUTHENTICATED"
}
}
the method itself consists of validation:
export const myFunctionName = regionFunctions.https.onCall(
async (request, context) => {
if (!userIsAuthenticated(context)) return {status: 401}
if (!userIsAdmin(context)) return {status: 403}
await syncCategories()
return {status: 200}
})
export const userIsAuthenticated = (context: CallableContext) => {
return context.auth
}
what I try to do, is to use server key from firebase project settings in request Authorization header.
Am I using wrong server key or what could be the issue?
Thanks a lot.
For callable functions, you're typically not supposed to deal with headers at all. The client SDK will handle all of that automatically for you, as shown in the documentation.
But if you do need to invoke a callable function directly using HTTPS, the Authorization header needs to be a Firebase Authentication user ID token as described in the protocol documentation. It is not a server key.
Thank you Doug,
your answer helped me to fix this issue.
What I have done:
Logged in to my application, through browser console looked at user ID token, copied it to Authorization header and it worked.
Yes, it is possible to use an API KEY from google cloud.
you must use this concept:
Request format: headers
The HTTP request to a callable trigger endpoint must be a POST with the following headers:
Optional: X-Firebase-AppCheck:
The Firebase App Check token provided by the client app making the request. The backend automatically verifies this token and decodes it, injecting the appId in the handler's context. If the token cannot be verified, the request is rejected. (Available for SDK >=3.14.0)
If any other headers are included, the request is rejected, as described in the response documentation below.

Firebase ID token has incorrect "aud" (audience) claim when calling from Endpoint connected to Google Functions

I am using Google Endpoints as an API gateway which is running in a Google Run container service. The API path points to a Google Function (node js). The calls to the API gateway are from a web application (viz. browser).
One of the paths is: /login which authenticates a user in firebase using the firebase.auth().signInWithEmailAndPassword method. I get the token Id of the user and send it back in the response header (authentication bearer) back to the browser. This works as expected.
When other Requests are made (e.g /check) to the endpoint the token (in the header) is included. I wanted to check the validity of the token using the Firebase Admin method before processing any requests. The code in the Google Function that does this for one of the routes is as follows:
...
const decodeIdToken = async (req, res, next) => {
// Read the ID Token from the Authorization header.
const idToken = req.headers.authorization.split('Bearer ')[1];
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
req.decodedToken = decodedIdToken;
next();
return;
} catch (error) {
return res.status(403).json({
status: 'failure',
data: null,
error: error.message
});
}
};
// use decodeIdToken as middleware
app.post('/check', decodeIdToken, (req, res, next) => {
return res.json({
status: 'success',
data: req.decodedToken,
error: null
});
});
When I call (via Postman ) the routes by directly calling the Google Function trigger both the routes work. However, when I call the Google Endpoints which point to the Google Function I receive the following error while using the Firebase Admin object:
Firebase ID token has incorrect \"aud\" (audience) claim. Expected \"PROJECT-ID\" but got \"https://us-central1-PROJECT-ID.cloudfunctions.net/FUNCTION-NAME\". Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token
When setting the Firebase Admin object in NodeJs I tried the following:
const admin = require('firebase-admin');
admin.initializeApp();
as well as
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://PROJECT-ID.firebaseio.com"
});
Use the X-Apigateway-Api-Userinfo Header
The header's value is the base64 encoded payload of the JWT. There's no need to reverify as API Gateway already verified the token for you and has made the contents available for your use.
Example for Node.js devs:
Buffer.from(req.header("x-apigateway-api-userinfo"), "base64").toString();
If for whatever reason you do need access to the original JWT, it is available in the X-Forwared-Authorization header.
Unnecessary Extra Credit:
To explain the error, the reason you are getting the wrong Audience claim is because the JWT you are trying to verify is a different JWT generated by API Gateway. The original Authorization Header has been replaced with this JWT. Why? It is telling Cloud Functions "Hey Cloud Function, it's me API Gateway that's calling you and here's a signed JWT to prove it". Hence API Gateway's audience ends up being the Cloud Function resource url whereas Firebase's audience is the Project the Firebase sits in.
Just another example of weird inconveniences due to Google's implementation if you ask me; they could have definitely left the Auth header untouched and had API Gateway use a different header, but beggars can't be choosers. 🤷‍♂️
Reference API Gateway Documentation:
Receiving authenticated results in your API
API Gateway usually forwards all headers it receives. However, it overrides the original Authorization header when the backend address
is specified by x-google-backend in the API config.
API Gateway will send the authentication result in the X-Apigateway-Api-Userinfo to the backend API. It is recommended to use
this header instead of the original Authorization header. This header
is base64url encoded and contains the JWT payload.
The following worked does not work (see comment below):
In the openapi-functions.yaml add the security defintion as recommended by the docs
securityDefinitions:
firebase:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
# Replace YOUR-PROJECT-ID with your project ID
x-google-issuer: "https://securetoken.google.com/YOUR-PROJECT-ID"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken#system.gserviceaccount.com"
x-google-audiences: "YOUR-PROJECT-ID"
Then, against the path (/check in my case), add the security section as below:
/check:
post:
...
x-google-backend:
....
....
security:
- firebase: []
....
Refer to: https://cloud.google.com/endpoints/docs/openapi/authenticating-users-firebase
There isn't problem with your admin-sdk settings, it's the idToken which is actually a jwt token retured as idToken while sign in using firebase.
Your problem is you are trying to use the JWT token returned as idToken by one of the auth() functions like firebase.auth().signInWithEmailAndPassword These do return a JWT token, however the auth claims will likely be wrong and won't pass verification by verifyIdToken. Firebase tech support confirmed this.
You have to use the firebase.auth().currentUser.getToken() function. That token will pass verification.
const idToken=await firebase.auth().currentUser.getToken()

401 Error when sending data to Stripe `Customers` API

I want to create a new customer in Stripe upon form submit and add that customer's credit card to their account. As of now, I'm using the following code upon submit in my React App. The create customer call is then made separately from my server:
async submit(ev) {
let {token} = await this.props.stripe.createToken({name: "Name"});
let response = await fetch("https://api.stripe.com/v1/customers", {
method: "POST",
headers: {"Content-Type": "text/plain"},
body: token.id
});
When sending that data, I get a 401 error on the let response = ... line. I know that a 401 is an auth error, but my test API keys are definitely correct and don't have limits on how they can access my stripe account. Can anyone advise?
The issue here is that you are trying to create a Customer object client-side in raw Javascript. This API request requires your Secret API key. This means you can never do this client-side, otherwise anyone could find your API key and use it to make refunds or transfer for example.
Instead, you need to send the token to your own server. There, you will be able to create a Customer or a Charge using one of Stripe's official libraries instead of making the raw HTTP request yourself.
In my case, it's throwing the error due to a missing of stripe public key
var stripe = Stripe('{{ env("STRIPE_KEY") }}');
then I pass the public key as above, and it worked like a charm.

authentication header vs query parameter in google cloud endpoints

I have tried everything, yet I cannot access my API using google cloud endpoints using a Authentication:Bearer header. According to Cloud Endpoints Docs:
When you send a request using an authentication token, for security reasons, we recommend that you put the token in the Authorization:Bearer header.
it also says:
If you cannot use the header when sending the request, you can put the authentication token in a query parameter called access_token.
I can perfectly access the API using access_token=" +idToken in my URL. However, when I try to send an HTTP request with the Authentication header like this:
const url =
"https://<PROJECTNAME>.appspot.com/getbalance";
axios
.get(url,{headers:{'Authentication':'Bearer '+idToken}})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.log(error);
});
I get this error:
JWT validation failed: Missing or invalid credentials
Is sending the token in a query parameter as safe as sending it in the header?
Your code example shows you setting an Authentication header, not an Authorization header. You should not typically use a query parameter as it will likely get logged in Cloud Console.
When using "Authorization: Bearer ", you would need to use an access token obtained through OAuth 2.0 authentication.
This can be illustrated if you use the Oauth Playground agains any of the Google APIs.
Keep in mind that if you want to access your Firebase database using the Oauth Playground, you would need to configure the client ID and client Secret of your Firebase project on the gear icon at the top right of the playground screen.
Also make sure to use these scopes:
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/firebase.database
After completing all the steps, you will be able to make a REST request using the authorization header with the obtained access token.

Caching Azure Mobile Service Token for Future Requests - Facebook and Error 400

Following the various examples in the documentation authentication using the azure mobile client services for javascript works fine. I am unable to persist the returned auth token so that it can be used to see if the user is still logged in on the next request.
Starting with the initial request to login all works fine:
client.login("facebook").then(function (results) {
console.log(results);
localStorageService.add('currentUser', client.currentUser);
}, function (error) {
console.log(error);
});
This returns an object that looks like this in the results and client.currentUser:
{
mobileServiceAuthenticationToken: "IHAVETRUNCATEDIT",
userId: "Facebook:1210971539"
}
I am storing this object into localstorage (or a cookie) so that the next time a login is required I can check this token exists and pass it back to the login client service (see the token part). According to various pages the format of this at least for the facebook provider should be in the form:
{"access_token": "THEACCESSTOKEN"}
Therefor this is what is being submitted when calling the login the second (persisted) time. The token being passed in is the same one that we placed in localstorage.
var currentUser = localStorageService.get('currentUser');
client.login("facebook",
{ "access_token": currentUser.mobileServiceAuthenticationToken })
.then(function (results) {
console.log(results);
}, function (error) {
console.log(error);
});
The error returned is:
Error: The Facebook Graph API access token authorization request failed with HTTP status code 400
I am not quite following how on the subsequent request (next day) to check to see if the user's token is still good.
It seems that the way to do this is the following:
client.currentUser = {
userId: currentUser.userId,
mobileServiceAuthenticationToken: currentUser.mobileServiceAuthenticationToken
};
There is no need to login again but just set the currentUser with the saved credentials.
The following blog post I used as a reference.
At least for Facebook, the access token returned from successful MobileServiceClient.login() request is not valid for further communication with Facebook API. Seems this is due the changes with FB Graph API done in March 2014 with their moving to version 2.2. What you can do is to perform manual login witn FB rather than use MobileServiceClient.login() and then set the obtained username and JWT to MobileServiceClient like you did:
client.currentUser = {
userId: "Facebook:xxx",
mobileServiceAuthenticationToken: "<your-users-JWT>"
};

Resources