FCM not working in Firebase Cloud Function because admin sdk authentication-error - firebase-cloud-messaging

I am currently trying to send FCM using Firebase Cloud Function in the Flutter WEB environment. But it doesn't work very well
My Problem
My index.js Code
/* eslint-disable max-len */
const admin = require("firebase-admin");
const {applicationDefault} = require("firebase-admin/app");
const functions = require("firebase-functions");
const cors = require("cors")({origin: true});
admin.initializeApp({credential: applicationDefault(), databaseURL: "https://<PROJECT-ID>.firebaseio.com"});
const messaging = admin.messaging();
exports.sendNoti = functions.runWith({
allowInvalidAppCheckToken: true}).https.onRequest(async (req, res) => {
cors(req, res, async () =>{
if (res.app == undefined) {
console.log("App Check failed-precondition / The function must be called from an App Check verified app.");
}
console.log(res.app);
try {
await messaging.sendToDevice(req.body.data.targetDevices, {
notification: {
title: req.body.data.messageTitle,
body: req.body.data.messageBody,
},
});
res.set("Access-Control-Allow-Origin", "*");
res.status(200).send({"status": "success", "data": "GOOD"});
return true;
} catch (ex) {
console.log(ex);
res.set("Access-Control-Allow-Origin", "*");
res.status(200).send({"status": "fail", "data": ex});
return ex;
}
});
});
Error Message
{errorInfo: {code: messaging/authentication-error, message: An error occurred when trying to authenticate to the FCM servers. Make sure the credential used to authenticate this SDK has the proper permissions. See https://firebase.google.com/docs/admin/setup for setup instructions. Raw server response: "HTML CODE
". Status code: 401.}, codePrefix: messaging}
My Try
credential: admin.credential.applicationDefault(),
credential: admin.credential.cert("service.json"),
Use OnCall function
Use AppCheck

Related

How to consume Next.JS Rest Endpoints secured with Amplify from a React Native app

Background:
My current stack is a Next server to use as an admin portal and REST API for a Mobile App running with Expo - React Native. The Next Server is currently hosted as a Lambda#Edge.
I have secured both the Next server and the React Native app with AWS Amplify's withAuthenticator wrapper. (I also tried specific auth packages like Next-Auth and Expo's auth package)
Problem:
However, I can't figure out how to add the Auth info (Access_token) to my REST Requests from Mobile app -> Next Server
I tried adding the tokens as bearer headers to the API without luck after that I was fairly sure it all has to be set up and sent via cookies.
BUT I am stuck on how to actually implement these cookies properly. I was hoping the endpoints:[] config could be used to set up my own domain to post to and handle the cookies. Reading the request on the server showed that it contained no Auth info when posted with this method.
Likewise using RTK Query (Preferably I add all the Auth to this instead of Amplify's API setup) I don't have the correct info to make an Authorized api request
Here are some snippets of the working page Authentication for both apps
API Endpoint /api/version:
import type { NextApiRequest, NextApiResponse } from 'next'
import { withSSRContext } from 'aws-amplify'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data | Error>,
) {
const { Auth } = withSSRContext({req})
try {
const user = await Auth.currentAuthenticatedUser()
return res.status(200).json({
version: '1.0.0',
user: user.username,
})
} catch (err) {
console.log(err)
return res.status(200).json({
message: 'Unauthenticated',
})
}
}
Mobile App Config:
import {
useAuthenticator,
withAuthenticator,
} from '#aws-amplify/ui-react-native'
import { Amplify, Auth } from 'aws-amplify'
import awsconfig from './aws-exports'
Amplify.configure({
...awsconfig,
API: {
endpoints: [
{
name: 'MyApi',
endpoint: 'http://NextIP:NextPort/api/',
},
],
},
})
Auth.configure(awsconfig)
export default withAuthenticator(App)
Mobile Screen:
import { API } from 'aws-amplify'
function getData() {
const apiName = 'MyApi'
const path = '/version'
const myInit = {
headers: {}, // OPTIONAL
}
return API.get(apiName, path, myInit)
}
export default function ModalScreen() {
// Get token / Cookie for auth
// const { data, isLoading, error } = useGetApiVersionQuery(null) // RTK Query
getData() // Amplify
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error.response)
})
return ( <></>)}
I found a solution, however, could not get the Next-Auth middleware to fire when the token was sent using the Bearer token in headers. Which is my ideal way of handling the routes.
I wrapped the getToken({req}) call so that if there is no JWT Web token it would try encode the token separately
Lastly ChatGpt somehow got me onto the package aws-jwt-verify which has everything you need to verify a token generated by aws-amplify/auth, in my case from react-native.
components/utils/auth.utils.ts
import { NextApiRequest } from 'next'
import { CognitoJwtVerifier } from 'aws-jwt-verify'
import { getToken } from 'next-auth/jwt'
// Verifier that expects valid token:
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USERPOOL_ID ?? '',
tokenUse: 'id',
clientId: process.env.COGNITO_CLIENT_ID ?? '',
issuer: process.env.COGNITO_ISSUER ?? '',
})
export async function getMobileToken(req: NextApiRequest) {
let token = null
try {
token = await getToken({ req })
} catch (error) {
console.log('Could not get JWT Web Token')
}
try {
if (!token)
token = await getToken({
req,
async decode({ token }) {
if (!token) return null
const decoded = await verifier.verify(token)
return decoded
},
})
} catch (error) {
return null
}
console.log('Mobile Token:', token)
return token
}

Microsoft Graph API calls through Express app

I am trying to follow this tutorial here: https://github.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/tree/main/2-Authorization/1-call-graph. It uses the microsoft-identity-express wrapper for msal-node, but I am having trouble with some Graph routes.
I have set up my Azure AD app permissions exactly as described in the tutorial.
I have successfully got the /profile route working, like this:
router.get('/profile',
msid.isAuthenticated(),
msid.getToken({
resource: appSettings.protectedResources.graphAPI
}),
async (req,res,next) => {
let output;
try {
const graphClient = graphManager.getAuthenticatedClient(req.session.protectedResources["graphAPI"].accessToken);
output = await graphClient
.api('/me')
.get();
} catch (error) {
console.log(error);
next(error);
}
res.send(output);
}
);
My appSettings look like this:
const appSettings = {
appCredentials: {
clientId: "zzz",
tenantId: "yyy",
clientSecret: "xxx"
},
authRoutes: {
redirect: "/redirect",
error: "/error", // the wrapper will redirect to this route in case of any error
unauthorized: "/unauthorized" // the wrapper will redirect to this route in case of unauthorized access attempt
},
protectedResources: {
graphAPI: {
endpoint: "https://graph.microsoft.com/v1.0/me",
scopes: ["user.read"]
},
armAPI: {
endpoint: "https://management.azure.com/tenants?api-version=2020-01-01",
scopes: ["https://management.azure.com/user_impersonation"]
},
joinedTeams: {
endpoint: "https://graph.microsoft.com/v1.0/me/joinedTeams",
scopes: ["user.read", "teamsettings.readwrite.all"]
},
myEvents: {
endpoint: "https://graph.microsoft.com/v1.0/me/events",
scopes: ["user.read", "calendars.readwrite"]
},
messages: {
endpoint: "https://graph.microsoft.com/v1.0/me/messages",
scopes: ["Mail.ReadWrite"]
}
}
};
And finally, my getAuthenticatedClient function from graphManager is:
const getAuthenticatedClient = (accessToken) => {
// Initialize Graph client
const client = graph.Client.init({
// Use the provided access token to authenticate requests
authProvider: (done) => {
done(null, accessToken);
}
});
return client;
};
graphManager and appSettings are set up exactly how the tutorial does it. I added more API endpoints to the protectedResources object as the tutorial says. I followed the same logic process as the /profile route for the extra Graph API routes. But I still get 401 errors whenever running this:
router.get('/me/events',
msid.isAuthenticated(),
msid.getToken({
resource: appSettings.protectedResources.myEvents
}),
async (req,res,next) => {
let output;
try {
const graphClient = graphManager.getAuthenticatedClient(req.session.protectedResources["myEvents"].accessToken);
output = await graphClient
.api('/me/events')
.get();
} catch (error) {
console.log(error);
next(error);
}
res.send(output);
}
);
I can't understand why /me calls to the Graph API work but /me/events does not. The correct permissions are being passed to msid.getToken() and getAuthenticatedClient(), so why do the API calls fail?

Firestore cloud functions apollo graphql authentication

I need help getting my Firebase Apollo/GraphQL Cloud Function to authenticate and receive query requests.
I implemented an Apollo/GraphQL server as a Cloud Function in
Firebase/Firestore using this repository from this post.
I set permissions for the cloud function to
allAuthenticatedUsers and I am using Firebase Phone
Authentication to authenticate.
I used code from this stackoverflow answer to help structure the
authentication portion not included in the initial repository.
The Apollo/GraphQL function works fine (tested with playground) when permissions are set to allUsers. After setting permissions to allAuthenticatedUsers and attempting to send authenticated queries I am receiving the following error response:
Bearer error="invalid_token" error_description="The access token could not be verified"
I believe I am making a mistake with the request sent by the client, and or the handling of the verification and "context" of the ApolloServer. I have confirmed the initial user token is correct. My current theory is that I am sending the wrong header, or messing up the syntax somehow at either the client or server level.
To explain what I believe the appropriate flow of the request should be:
Token generated in client
Query sent from client with token as header
ApolloServer cloud function receives request
Token is verified by Firebase, provides new verified header token
Server accepts query with new verified header token and returns data
If anyone can explain how to send valid authenticated client queries to a Firebase Apollo/GraphQL Cloud Function the help would be greatly appreciated. Code for server and client below.
Server.js (ApolloServer)
/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "[db-url]",
});
/* Async verification with user token */
const verify = async (idToken) => {
var newToken = idToken.replace("Bearer ", "");
let header = await admin.auth().verifyIdToken(newToken)
.then(function(decodedToken) {
let uid = decodedToken.uid;
// Not sure if I should be using the .uid from above as the token?
// Also, not sure if returning the below object is acceptable, or
// if this is even the correct header to send to firebase from Apollo
return {
"Authorization": `Bearer ${decodedToken}`
}
}).catch(function(error) {
// Handle error
return null
});
return header
}
/* Server */
function gqlServer() {
const app = express();
const apolloServer = new ApolloServer({
typeDefs: schema,
resolvers,
context: async ({ req, res }) => {
const verified = await verify(req.headers.Authorization)
console.log('log verified', verified)
return {
headers: verified ? verified: '',
req,
res,
}
},
// Enable graphiql gui
introspection: true,
playground: true
});
apolloServer.applyMiddleware({app, path: '/', cors: true});
return app;
}
export default gqlServer;
Client.js (ApolloClient)
Client query constructed using these instructions.
/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
const userToken = await user.getIdToken();
/* Client creation */
const client = new ApolloClient({
uri: '[Firebase Cloud Function URL]',
headers: {
Authorization: userToken ? `Bearer ${userToken}` : ''
},
cache: new InMemoryCache(),
});
/* Query test */
client.query({
query: gql`
{
hello
}
`
}).then(
(result) => console.log('log query result', result)
).catch(
(error) => console.log('query error', error)
)
})
UPDATE 05/03/20
I may have found the source of the error. I won't post an answer until I confirm, but here's the update. Looks like allAuthenticatedUsers is a role specific to Google accounts and not Firebase. See this part of the google docs and this stackoverflow answer.
I will do some testing but the solution may be to change the permissions to allUsers which may still require authentication. If I can get it working I will update with an answer.
I was able to get things working. Working requests required the following changes:
Change cloud function "invoker" role to include allUsers instead of allAuthenticatedUsers. This because the allUsers role makes the function available to http requests (you can still require authentication through sdk verification)
Adjusting the code for the server and client as shown below. Minor change to header string construction.
Server.js (ApolloServer)
/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "[db-url]",
});
/* Async verification with user token */
const verify = async (idToken) => {
if (idToken) {
var newToken = idToken.replace("Bearer ", "");
// var newToken = idToken
let header = await admin.auth().verifyIdToken(newToken)
.then(function(decodedToken) {
// ...
return {
"Authorization": 'Bearer ' + decodedToken
}
}).catch(function(error) {
// Handle error
return null
});
return header
} else {
throw 'No Access'
}
}
/* Server */
function gqlServer() {
const app = express();
const apolloServer = new ApolloServer({
typeDefs: schema,
resolvers,
context: async ({ req, res }) => {
// headers: req.headers,
const verified = await verify(req.headers.authorization)
console.log('log verified', verified)
return {
headers: verified ? verified: '',
req,
res,
}
},
// Enable graphiql gui
introspection: true,
playground: true
});
apolloServer.applyMiddleware({app, path: '/', cors: true});
return app;
}
export default gqlServer;
Client.js (ApolloClient)
/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
const userToken = await user.getIdToken();
/* Client creation */
const userToken = await user.getIdToken();
const client = new ApolloClient({
uri: '[Firebase Cloud Function URL]',
headers: {
"Authorization": userToken ? 'Bearer ' + userToken : ''
},
cache: new InMemoryCache(),
});
client.query({
query: gql`
{
hello
}
`
}).then(
(result) => console.log('log query result', result)
).catch(
(error) => console.log('query error', error)
)
})

Unable to make APEX Webservice callouts from Dialogflow Intent Handler

I have an Express App ( hosted on Heroku ) which i'm using to handle intents from Dialogflow and make callouts to APEX REST Webservice classes (to get data from Salesforce) and then show the results back on Google Assistant.
For authentication, i'm trying to implement OAuth, and hence I've created Connected App on Salesforce.
On Google Actions under Account Linking i've mentioned the 'Authorization URL' as Express App URL (something like https://testBot.herokuapp.com/authorization) and 'Client Id issued by your Actions to Google' as Consumer Key from Salesforce Connected App and lastly 'Client Secret' as Salesforce Connected App Consumer Secret. Also, my Token URL is like https://testBot.herokuapp.com/token.
On Express i've created routes, first to handle the request coming in for authorization (to get authorization code) and then secondly on the callback route (this is the callback URL on Salesforce Connected App) as mentioned on Implement OAuth account linking i've redirected to redirect_uri (of the form https://oauth-redirect.googleusercontent.com/r/MY_PROJECT_ID) with authorization code and state as parameters. This is how the uri looks https://oauth-redirect.googleusercontent.com/r/MY_PROJECT_ID?code=AUTHORIZATION_CODE&state=STATE_STRING. Now on the 3rd route (https://testBot.herokuapp.com/token), logic is written to exchange authorization code for an access token and a refresh token. Note that the token exchange endpoint responds to POST requests.
Now as per official documentation , Google stores the access token and the refresh token for the user. So, what this means is that Conversation or conv object should hold the access token values however when I try to access the same and then make a callout to the APEX Webservice I could see that conv.user.accessToken gives undefined and hence the callout is also unsuccessful (error : INVALID_SESSION_ID: Session expired or invalid) even after successful authentication.
My question is why i'm not getting the access token from CONV and if this is expected (am I reading the documentation incorrectly) how am I supposed to get the access token ?
Here is the express code:
const express = require('express');
const bodyParser = require('body-parser');
const jsforce = require('jsforce');
const { dialogflow } = require('actions-on-google');
const {
SimpleResponse,
BasicCard,
SignIn,
Image,
Suggestions,
Button
} = require('actions-on-google');
var options;
var timeOut = 3600;
var port = process.env.PORT || 3000;
var conn = {};
const expApp = express().use(bodyParser.json());
expApp.use(bodyParser.urlencoded());
//app instance
const app = dialogflow({
debug: true
});
const oauth2 = new jsforce.OAuth2({
clientId: process.env.SALESFORCE_CONSUMER_KEY,
clientSecret: process.env.SALESFORCE_CONSUMER_SECRET,
redirectUri: 'https://testbot.herokuapp.com/callback'
});
expApp.get('/authorize', function(req, res) {
var queryParams = req.query;
console.log('this is the first request: '+req);
res.redirect(oauth2.getAuthorizationUrl({ state: queryParams.state }));
});
expApp.get('/callback', function(req,res) {
var queryParams = req.query;
console.log('Request came for access callback');
console.log('Query params in callback uri is ', req.query);
let redirectUri = `${process.env.GOOGLE_REDIRECT_URI}?code=${queryParams.code}&state=${queryParams.state}`;
console.log('Google redirecturi is ', redirectUri);
res.redirect(redirectUri);
});
expApp.post('/token', function(req, res) {
console.log('Request came for accesstoken');
console.log('query params are-->', req.body);
console.log('req query-->', req.query);
res.setHeader('Content-Type', 'application/json');
if (req.body.client_id != process.env.SALESFORCE_CONSUMER_KEY) {
console.log('Invalid Client ID');
return res.status(400).send('Invalid Client ID');
}
if (req.body.client_secret != process.env.SALESFORCE_CONSUMER_SECRET) {
console.log('Invalid Client Ksecret');
return res.status(400).send('Invalid Client ID');
}
if (req.body.grant_type) {
if (req.body.grant_type == 'authorization_code') {
console.log('Fetching token from salesforce');
oauth2.requestToken(req.body.code, (err, tokenResponse) => {
if (err) {
console.log(err.message);
return res.status(400).json({ "error": "invalid_grant" });
}
console.log('Token respons: ',tokenResponse);
var googleToken = {
token_type: tokenResponse.token_type,
access_token: tokenResponse.access_token,
refresh_token: tokenResponse.refresh_token,
expires_in: timeOut
};
console.log('Token response for auth code', googleToken);
res.status(200).json(googleToken);
});
}
else if (req.body.grant_type == 'refresh_token') {
console.log('Fetching refresh token from salesforce');
oauth2.refreshToken(req.body.refresh_token, (err, tokenResponse) => {
if (err) {
console.log(err.message);
return res.status(400).json({ "error": "invalid_grant" });
}
console.log('Token response in refresh token: ',tokenResponse);
var googleToken = { token_type: tokenResponse.token_type, access_token: tokenResponse.access_token, expires_in: timeOut };
console.log('Token response for auth code', googleToken);
res.status(200).json(googleToken);
});
}
} else {
res.send('Invalid parameter');
}
});
var createTask = function(oppName,taskSubject,taskPriority,conFName,conn){
return new Promise((resolve,reject)=>{
conn.apex.get("/createTask?oppName="+oppName+"&taskSubject="+taskSubject+"&taskPriority="+taskPriority+"&contactFirstName="+conFName,function(err, res){
if (err) {
console.log('error is --> ',err);
reject(err);
}
else{
console.log('res is --> ',res);
resolve(res);
}
});
});
};
app.intent('Default Welcome Intent', (conv) => {
console.log('Request came for account link flow start');
if(!conv.user.accessToken){
conv.ask(new SignIn());
}
else{
conv.ask('You are already signed in ');
}
});
app.intent('Get SignIn Info', (conv, params, signin) => {    
console.log('Sign in info Intent');    
console.log('Sign in content-->',signin);       
if (signin.status === 'OK') {         
conv.ask('Hola, thanks for signing in! What do you want to do next?')       ;
} 
else {         
conv.ask('Something went wrong in the sign in process');       
}     
});
app.intent('Create Task on Opportunity', (conv, {oppName,taskSubject,taskPriority,contactFirstName} ) => {
console.log('conv: ',conv);
//this logs undefined
console.log('Access token from conv inside intent: ',conv.user.accessToken);
const opName = conv.parameters['oppName'];
const tskSbj = conv.parameters['taskSubject'];
const tskPr = conv.parameters['taskPriority'];
const conFName = conv.parameters['contactFirstName'];
console.log('Instance URL as stored in heroku process variable: ',process.env.INSTANCE_URL);
conn = new jsforce.Connection({
instanceUrl : process.env.INSTANCE_URL,
accessToken : conv.user.accessToken
});
return createTask(opName,tskSbj,tskPr,conFName,conn).then((resp) => {
conv.ask(new SimpleResponse({
speech:resp,
text:resp,
}));
});
});
expApp.get('/', function (req, res) {
res.send('Hello World!');
});
expApp.listen(port, function () {
expApp.post('/fulfillment', app);
console.log('Example app listening on port !');
});
So, on logging conversation.user I understood that conv.user.access.token is correct and not conv.user.accessToken. Hence, now the connection instance would look like:
conn = new jsforce.Connection({
instanceUrl : process.env.INSTANCE_URL,
accessToken : conv.user.acces.token
});
Now, get request on apex web service does send expected response !

Skype Web API sing in issues using login and password

I have issue with signing into Skype account using login and password. I use code from Skype Web SDK docs:
<script>
// Call the application object
var config = {
apiKey: 'a42fcebd-5b43-4b89-a065-74450fb91255', // SDK
apiKeyCC: '9c967f6b-a846-4df2-b43d-5167e47d81e1' // SDK+UI
};
var Application;
Skype.initialize({ apiKey: config.apiKey }, function (api) {
window.skypeWebApp = new api.application();
window.skypeWebApp.signInManager.signIn({
username: "test#yandex.ru",
password: "12345678"
}).then(function () {
console.log('Signed in successfully.');
}, function (error) {
console.log('Failed to sign in.');
}).then(reset);
}, function (err) {
console.log(err);
alert('Cannot load the SDK.');
});
The POST request is sent, but API returns nothing. Can anybody advice?