Firestore cloud functions apollo graphql authentication - react-native

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)
)
})

Related

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

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

How to redirect to login page if JWT is not valid, using Express with API

I've built an Express app that contains an API and a front end. By using Axios the front end can request data (e.g. a user-object or a todo-object) from the API, which will validate the offered JWT with its middleware. If the jwt.verify() errs, the protected routes won't fire. This all works fine.
My question is: how do I set up the front end such that any page-request will redirect to a login page if the browser-stored JWT is not valid (excluding the login and register pages, to prevent circular redirection)? Do I have to preface every .ejs-file with an Axios.post() that sends the browser-stored JWT for verification, or is there a best practice that I am missing?
My goal, when an invalid JWT is offered, is to have the API routes return a json-object (e.g. { err: "invalid token offered" }), and to have all the front end routes redirect the user to the login page.
Some sample code below.
server.js
// API Routes
app.use('/api/todos', CheckToken, APITodosRouter)
app.use('/api/auth', APIAuthRouter)
// Front-end Routes
app.use('/', indexRouter)
app.use('/todos', todosRouter)
app.use('/auth', authRouter)
todos.ejs (This works fine)
// get todos from db
let todosData
const getTodos = async () => {
let response = await axios.get('/api/todos/all', {
headers: {
'Content-Type': 'application/json',
'authorization': `Bearer ${localStorage.access_token}`
}
})
if (!response) return console.log({ msg: "no response received."})
if (!response.data) return console.log({ msg: "no data received."})
if (!response.data.payload) return console.log({ msg: "no todos found."})
todosData = response.data.payload
}
// boot page
;(async () => {
await getTodos()
renderTodos() // a function that reads todosData updates the DOM accordingly
})()
checkToken.js (Middleware)
const jwt = require('jsonwebtoken')
const checkToken = (req, res, next) => {
const ah = req.headers.authorization
const token = ah && ah.split(' ')[1]
if (!token) return res.json({ msg: "No token offered."})
jwt.verify(token, process.env.TOKEN_SECRET, (err, user) => {
if (err) return res.json({ msg: "Invalid token offered."})
req.user = user
next()
})
}
module.exports = checkToken

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 !

Oauth2 Google Authentication flow - Next.JS / Express

I am using a React/Next.Js Frontend and am trying to implement authentication with the Oauth2 strategy with Google.
I am very confused by the process.
Currently on the client, I have a Google sign in component that has a Client ID with in it and can retrieve an access token.
<GoogleLogin
clientId="myclientid"
buttonText="Login"
onSuccess={userLogin}
onFailure={userLogin}
cookiePolicy={'single_host_origin'}
/>
I then have a function, which on success sends a post message to my backend with an access token, such as this:
export function googleAuthenticate(accessToken : string) : any{
axios({
method: 'post',
url: "http://localhost:4000/auth/google",
data: {
accessToken: accessToken
}
})
.then(res => {
console.log(res);
})
.catch(err => {
console.log("Failure!");
console.log(err);
})
};
On the backend I am using passport, and the routes look like this:
import express from 'express';
import passport from 'passport';
import Logger from '../logger/index';
const router = express.Router();
export function isAuthenticated(req:express.Request, res:express.Response, next : any) {
return req.isAuthenticated() ?
next() :
res.sendStatus(401);
}
router.get('/fail', (_req:express.Request, res:express.Response) => {
res.json({ loginFailed: true });
});
router.post('/google', passport.authenticate('google', { scope: ['profile']}), (_req:express.Request, _res:express.Response) => {
Logger.info("GET Request at Google Authentication endpoint received.");
});
router.get(
'/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(_req:express.Request, res:express.Response) => {
res.redirect('/graphql');
}
);
export default router;
My passport module looks like this:
module.exports = function(passport : any, GoogleStrategy : any){
passport.use(new GoogleStrategy({
clientID: config.google.client_id,
clientSecret: config.google.client_secret,
callbackURL: config.google.redirect_url
},
function(accessToken : string, profile : Profile, refreshToken : string, cb : any) {
return cb(null, {
id: profile.googleId,
username: profile.email,
image: profile.imageUrl,
firstName: profile.givenName,
surname: profile.familyName,
accessToken: accessToken,
refreshToken: refreshToken
})
}
));
}
Since Next.js is a server side rendered, I am not able to use save a token. I understand I have to use a cookie. But how does this work? I cannot redirect the client browser from the express backend.
Currently I'm just seeing these 2 errors:
OPTIONS https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2localhost:3000%2Fdashboard&scope=profile&client_id=687602672235-l0uocpfchbjp34j1jjlv8tqv7jadb8og.apps.googleusercontent.com 405
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fbackoffice.dev.myos.co%2Fdashboard&scope=profile&client_id=687602672235-l0uocpfchbjp34j1jjlv8tqv7jadb8og.apps.googleusercontent.com' (redirected from 'http://localhost:4000/auth/google') from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Firstly i think google auth will not work on localhost.
If i understand correctly in your serverside logic you can easily save your token as a cookie and then read them in the client.
Not sure with passport, but you can do something similar to this :
(my app is working with an implementation of this code)
frontend :
<GoogleLogin
clientId="myclientid"
buttonText="Login"
onSuccess={userLogin}
onFailure={userLogin}
cookiePolicy={'single_host_origin'}
/>
userLogin:
async userLogin(response){
var url = '/google-login/'+response.tokenObj.id_token
fetch(url).then(/* i will handle response*/)
}
Then in the backend you can use google-auth-library to login or register.
server.js:
const {OAuth2Client} = require('google-auth-library');
const GOOGLEID = "mygoogleid.apps.googleusercontent.com"
const client = new OAuth2Client(GOOGLEID);
var cookieParser = require('cookie-parser')
async function verify(userToken) {
const ticket = await client.verifyIdToken({
idToken: userToken,
audience: "clientid.apps.googleusercontent.com", // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
const userid = payload['sub'];
return payload
// If request specified a G Suite domain:
//const domain = payload['hd'];
}
In server.js a route similar to this :
server.get('/google-login/:token',(req,res) => {
const userToken = req.params.token
var result = verify(userToken).then(function(result){
var userName = result.given_name
var userSurname = result.family_name
var userEmail = result.email
/*
Now user is authenticated i can send to the frontend
user info or user token o save the token to session
*/
}).catch(function(err){
// error handling
})
})
You could use NextAuth.js to handle this for you.
In order to test localhost you should use ngrok to expose your localhost server to the web and configure the given url in google platform

Automatically log out user when token is invalidated

I have a SPA that is built on vuejs. When a user is logged in via API, the token is stored in local storage.
I need a global solution which will logout and prompt the user when the token is no longer valid. At the moment, I get "invalid token" error when accessing private API endpoints.
How do I rig axios so that ALL response of invalid tokens will trigger the logout/prompt code?
Here is an simple example with axios. It use a Bearer token for authentification.
import axios from "axios";
import { useUserStore } from "#/store/userStore";
const apiClient = axios.create({
baseURL: ""http://127.0.0.1:8001",
headers: {},
});
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const config = error?.config;
if (error?.response?.status === 401) {
const result = await refreshToken();
if (result) {
config.headers = {
...config.headers,
authorization: `Bearer ${result?.token}`,
};
}
return axios(config);
}
);
const refreshToken = async () => {
/* do stuff for refresh token */
// if refresh token failed
try {
useUserStore().actionLogout();
} catch (error) {
console.log(error);
} finally {
loacalStorage.clear();
}
};
you can write a function that clears your local storage after some time and logout user