React native app through Expo, getting firestore permission error: - ios

This is my first post here so please let me know if I'm not posting this correctly.
I keep getting the following error in the debug logs of my react native Expo app on the iOS simulator when I have an authenticated user trying to retrieve a firestore document:
FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
Here is firebase.js config file:
import "firebase/firestore";
import "firebase/storage";
import * as firebase from 'firebase';
// Initialize Firebase
const firebaseConfig = {
apiKey: ... //removed for this post, but it is correct and validated
};
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
const auth = firebase.auth();
export { auth };
export default db;
Here is my App.js:
import React, { useEffect, useState } from 'react';
import db, { auth } from './firebase';
const getUserData = async(uid) => {
try {
const doc = await db.collection('users').doc(uid).collection('info').doc(uid).get();
if (doc.exists) {
console.log(doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No user info was found for the authenticated user");
}
} catch(err) {
console.log(err);
}
};
useEffect(() => {
auth.onAuthStateChanged((authUser) => {
if (authUser) {
//user is logged in
getUserData(authUser.uid); //retrieve the user's profile data
} else {
//user is logged out
auth.signOut();
}
});
}, []);
My security rules shouldn't be the problem because it works for my web react app with the same logic and user, and the get request is only sent when there is a uid because the user is authenticated. I've printed out the uid after onAuthStateChanged and it is the correct uid.
//Security Rules in Firestore
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedInAndSameUser(uid) {
return request.auth != null && request.auth.uid == uid;
}
match /users/{uid} {
allow read: if request.auth != null;
match /private/{privateId} {
allow read: if signedInAndSameUser(privateId);
}
}
I've seen similar posts that recommended to downgrade to firebase#4.6.2 but I also ran into issues and couldn't get it to work. I'm wondering if firebase still hasn't fixed this issue even after version 8 (In react native app (through Expo) using firestore permissions - request.auth is always null)
This is my current firebase and expo version in my package.json:
//package.json
"expo": "~41.0.1",
"firebase": "8.2.3",
Thank you so much if you can help, I've been stuck on this issue for many hours and can't seem to understand why this works in my react.js web app, but the same logic, user, and security rules won't work in my react native Expo iOS app.

Related

Firebase Auth throwing "auth/internal-error" when trying to use PhoneAuthProvider method in Ionic React IOS mobile application

I am currently having some issues trying to integrate Google Firebase authentication into a React Ionic mobile application. So far I have been able to set up the app to run correctly on both web and android but am running into repeated issues with IOS. The code runs correctly for Firebase auth login on android does not seem to be running on IOS.
The initial call to "signInWithEmailAndPassword" seems to work fine and returns "auth/multi-factor-auth-required" but any time I try to make a call to "verifyPhoneNumber" on ios I will receive the response of "Firebase: An internal AuthError has occurred. (auth/internal-error)", there are no further details provided in any part of the error returned.
//Firebase setup in seperate file with code like these 2 lines
app.initializeApp(config);
const firebaseAuth = app.auth();
//
firebaseAuth
.signInWithEmailAndPassword(email, password)
.then(function (user) {
//other code
})
.catch(function (error) {
if (error.code === "auth/multi-factor-auth-required") {
let resolver = error.resolver;
setTimeout(() => {
phoneAuth(appVerifier, resolver);
}, 2000);
} else {
//other code
}
});
//following code is part of the method phoneAuth() above
var phoneInfoOptions = {
var phoneInfoOptions = {
multiFactorHint: resolver.hints[0],
session: resolver.session,
};
var phoneAuthProvider = new firebase.PhoneAuthProvider();
phoneAuthProvider
.verifyPhoneNumber(phoneInfoOptions, appVerifier)
.then(function (verificationId) {
console.log("verify Id recieved");
})
.catch((error) => {
console.log(error);
});
Things I have checked/tried so far are:
followed all steps in firebase docs
Setup ios app in firebase console and followed the steps listed
Has anyone run into issues like this previously and has any advice on what I could check next?
Thanks

Angular / Ionic mobile app ios does not fetch from Firebase using angularfire

I am trying to test a little Ionic/Angular sample app on an iOS Emulator.
On the web, all the requests to firestore using angularfire work perfectly fine.
Somehow if I try to execute the same app on the emulator, it keeps loading for the response of the request (if it was a empty response it would say that no results could be retrieved).
What is going on? Do i need to set something specifically for the Emulator to work and perform requests to Firestore?
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { Capacitor } from '#capacitor/core';
import { initializeAuth, indexedDBLocalPersistence } from 'firebase/auth';
import { getAuth } from 'firebase/auth';
const firebaseApp = initializeApp({
apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.VUE_APP_FIREBASE_DATABASE_URL,
projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId:
process.env.VUE_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.VUE_APP_FIREBASE_APP_ID,
});
function whichAuth() {
let auth
if (Capacitor.isNativePlatform()) {
auth = initializeAuth(firebaseApp, {
persistence: indexedDBLocalPersistence
})
} else {
auth = getAuth()
}
return auth
}
export const auth = whichAuth()
const db = getFirestore();
export const auth = whichAuth();
export { firebaseApp, db };
Then in your component, cal your method like this await signInAnonymously(auth);. Don't forget to import the auth we exported at the top.
[Edit: updated with instructions Firebase JS SDK version 9 (modular)]
This error occurs because Firebase Auth incorrectly detects its environment as a normal browser environment and tries to load remote Google APIs, which results in the error you see in the console:
TypeError: undefined is not an object (evaluating 'gapi.iframes.getContext')
Fortunately, Firebase Auth already has logic to handle running in Cordova/Ionic apps, you just need to tell it which platform it's on.
For Firebase JS SDK version 9 (modular)
Simply import the Cordova Firebase Auth implementation:
import { getAuth } from 'firebase/auth';
For Firebase JS SDK <9 or the compatibility modules (auth/compat)
In capacitor.config set server: { iosScheme: "ionic" }:
// capacitor.config.json
{
"server": {
"iosScheme": "ionic"
}
}
There's a check in the auth/compat library here which, when it sees the URL scheme "ionic://", uses its Ionic/Cordova loading logic, and otherwise falls back to normal browser logic which fails with the error above.
Recent versions of Capacitor changed the URL scheme to "capacitor://" which fails this test but you can override it in your capacitor.config file (see the config option iosScheme).
(See also #alistairheath's comment here).
Been struggling a lot with this issue too but I managed to fix it. For those who need help here's my code.
You can delete all Firebase related imports from app.module.ts since this solution only uses Firebase.
The packages rxfire and #angular/fire can be removed from your package.json. The only dependency I have is "firebase": "^9.6.1".
I used observables for the getObject and list functions since that's what I'm used to and I didn't want to rewrite my original code.
import { Injectable } from '#angular/core';
import { Capacitor } from '#capacitor/core';
import { environment } from '#environment';
import { initializeApp } from 'firebase/app';
import { Auth, getAuth, indexedDBLocalPersistence, initializeAuth, signInWithCustomToken } from 'firebase/auth';
import { Database, getDatabase, onValue, orderByChild, query, ref } from 'firebase/database';
import { Observable, Observer, from } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class FirebaseService {
private readonly database: Database;
private readonly auth: Auth;
constructor() {
const firebaseApp = initializeApp(environment.firebase);
if (Capacitor.isNativePlatform()) {
initializeAuth(firebaseApp, {
persistence: indexedDBLocalPersistence
});
}
this.database = getDatabase(firebaseApp);
this.auth = getAuth(firebaseApp);
}
connectFirebase(firebaseToken) {
return from(signInWithCustomToken(this.auth, firebaseToken));
}
disconnectFirebase() {
return from(this.auth.signOut());
}
getObject<T>(path: string): Observable<T> {
return new Observable((observer: Observer<T>) => {
const dbRef = ref(this.database, path);
const listener = onValue(dbRef, snapshot => {
const data = snapshot.val();
observer.next(data);
});
return {
unsubscribe() {
listener();
}
};
});
}
public list<T>(path: string, orderChildBy?: string): Observable<Array<T>> {
return new Observable<Array<T>>((observer: Observer<Array<T>>) => {
const dbRef = ref(this.database, path);
const dbReference = !orderChildBy ? dbRef : query(dbRef, orderByChild(orderChildBy));
const listener = onValue(dbReference, snapshot => {
const data = Object.values<T>(snapshot.val() || {});
console.log(path, data);
observer.next(data);
});
return {
unsubscribe() {
listener();
}
};
});
}
}
For those who can't see the error message thrown by firebase try the following command in your Safari console to see the error.
window.location.reload()
The real problem: firebase-js-sdk on mobile iOS assumes google API (gapi) exists on the window, even when it isn't used.
I found a work around: Mock window.gapi before using firebase auth login:
window['gapi'] = {
load: (name: string) => Promise.resolve(),
iframes: {
getContext: () => {
return {
iframe: {
contentWindow: {
postMessage: (message: any) => {
console.log("gapi iframe message:", message);
}
}
}
}
}
}
} as any;

React native expo implementing Apple App Tracking Transparency (ATT) for iOS 14.5

What is the best way of implementing the Apple App Transparency Tracker (ATT) feature on react native expo? My app keeps getting rejected by apple even after I add:
app.json file :
"infoPlist": {
"NSUserTrackingUsageDescription": "App requires permission...."
}
On iOS 14 Apple introduced the App Tracking Transparency permission to access IDFA.
You need to prompt the user whether it allows your app to use libraries that track them or not, adding it on infoPlist just allows you to use this API within your application.
Expo doesn't have this feature yet, but some libraries you can use to prompt the permission
Example: https://docs.expo.io/versions/v41.0.0/sdk/facebook/#facebookgetpermissionsasync
You can use other libraries , such as https://github.com/mrousavy/react-native-tracking-transparency
where you can request the App tracking like
import { getTrackingStatus } from 'react-native-tracking-transparency';
const trackingStatus = await getTrackingStatus();
if (trackingStatus === 'authorized' || trackingStatus === 'unavailable') {
// enable tracking features
}
import { requestTrackingPermission } from 'react-native-tracking-transparency';
const trackingStatus = await requestTrackingPermission();
if (trackingStatus === 'authorized' || trackingStatus === 'unavailable') {
// enable tracking features
}
This might need an update in a near future, as expo releases a new SDK version with a solution for that.
EDIT
From Expo 44+
Expo now have a library for TrackTransparency: (https://docs.expo.dev/versions/latest/sdk/tracking-transparency/)
expo install expo-tracking-transparency
For bare applications: https://github.com/expo/expo/tree/main/packages/expo-tracking-transparency#installation-in-bare-react-native-projects
You can add it as a plugin at your app.json
{
"expo": {
"plugins": [
[
"expo-tracking-transparency",
{
"userTrackingPermission": "This identifier will be used to deliver personalized ads to you."
}
]
]
}
}
And now you can use like this:
import React, { useEffect } from 'react';
import { Text, StyleSheet, View } from 'react-native';
import { requestTrackingPermissionsAsync } from 'expo-tracking-transparency';
export default function App() {
useEffect(() => {
(async () => {
const { status } = await requestTrackingPermissionsAsync();
if (status === 'granted') {
console.log('Yay! I have user permission to track data');
}
})();
}, []);
return (
<View style={styles.container}>
<Text>Tracking Transparency Module Example</Text>
</View>
);
}
You need to request Tracking permissions first (I used react-native-permissions):
import { request, RESULTS, PERMISSIONS } from 'react-native-permissions'
export const requestPermissionTransparency = async () => {
return await request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY)
}
useEffect(() => {
;(async () => {
const result = await requestPermissionTransparency()
if (result === RESULTS.GRANTED) {
//You need to enable analytics (fb,google,etc...)
await firebase.analytics().setAnalyticsCollectionEnabled(true)
console.log('Firebase Analytics: ENABLED')
}
})()
}, [])
Remember to add this file in the root project:
// <project-root>/firebase.json
{
"react-native": {
"analytics_auto_collection_enabled": false
}
}
References: https://rnfirebase.io/analytics/usage
The solution I ended up using from expo was using the Facebook.getPermissionsAsync()
https://expo.canny.io/feature-requests/p/expo-permissions-add-support-to-apptrackingtransparency-permission-on-ios
Expo 41+
TrackingTransparency:
https://docs.expo.io/versions/latest/sdk/tracking-transparency/
import { requestTrackingPermissionsAsync } from 'expo-tracking-transparency';
const { status } = await requestTrackingPermissionsAsync();
if (status === 'granted') // do something
Expo 40 and below
Admob: https://docs.expo.io/versions/latest/sdk/admob/
import { requestPermissionsAsync } from 'expo-ads-admob'
const { status } = await requestPermissionsAsync()
if (status === 'granted') // do something

Get JWT token in NextJS API

I created a NextJS application integrated with Amazon Cognito. I have a landing page that is using the Amplify Auth API (not the components). Now I need to call an external API to do CRUD operations. What's the best way to do this in NextJS?
I'm thinking I'll create an API in NextJS that will forward the request to the actual external REST API. But my problem is I'm not able to get the JWT Token on the API, since it's a backend code.
A code like this:
Auth.currentSession().then(data => console.log(data.accessToken.jwtToken));
Obviously won't work:
[DEBUG] 20:42.706 AuthClass - Getting current session
[DEBUG] 20:42.706 AuthClass - Failed to get user from user pool
[DEBUG] 20:42.707 AuthClass - Failed to get the current user No current user
(node:20724) UnhandledPromiseRejectionWarning: No current user
How can I get the token in the API?
I have resolved this problem by using the aws-cognito-next library.
Following the documentation from https://www.npmjs.com/package/aws-cognito-next, I have created an auth utility:
import { createGetServerSideAuth, createUseAuth } from "aws-cognito-next";
import pems from "../../pems.json"
// create functions by passing pems
export const getServerSideAuth = createGetServerSideAuth({ pems });
export const useAuth = createUseAuth({ pems });
// reexport functions from aws-cognito-next
export * from "aws-cognito-next";
The pem file was generated by issuing the command (needless to say, you must configure an Amazon Cognito service first):
yarn prepare-pems --region <region> --userPoolId <userPoolId>
And finally, in the NextJs API:
import {getServerSideAuth} from "../../src/utils/AuthUtils"
export default async (req, res) => {
const initialAuth = getServerSideAuth(req)
console.log("initialAuth ", initialAuth)
if (initialAuth) {
res.status(200).json({status: 'success'})
} else {
res.status(400).json({status: 'fail'})
}
}
A simple method is to enable ssrContext in your app and Amplify will provide the user credentials to your api
on the frontend eg _app.tsx (or app.js)
import Amplify, { Auth, API } from "aws-amplify";
import awsconfig from "../src/aws-exports";
Amplify.configure({...awsconfig, ssr: true});
Then in the api you can simply get the currently authenticated cognito user
eg api/myfunction.tsx (or js)
import Amplify, { withSSRContext } from "aws-amplify";
import awsExports from "../../src/aws-exports";
Amplify.configure({ ...awsExports, ssr: true });
/* #returns <CognitoUser>,OR null if not authenticated */
const fetchAuthenticatedUser: any = async (req: NextApiRequest) => {
const { Auth } = withSSRContext({ req });
try {
let user = await Auth.currentAuthenticatedUser();
return user;
} catch (err) {
return null;
}
}

Firebase database not working with Facebook Expo authentication

I've been developing an app with React Native (with Expo) and Firebase on the backend. When running the project through Expo client on the iPhone, I can normally login with email and password and then fetch data from Firebase database. But when I login with Facebook, database read hands and it does not resolve anything. Important parts of the code look following:
firebase.initializeApp(firebaseConfig);
// This works everywhere
export const login = async (email, password) => {
await firebase.auth().signInWithEmailAndPassword(email, password);
const userId = firebase.auth().currentUser.uid;
return userId + '';
};
export const loginByFacebook = async () => {
const { type, token } = await Expo.Facebook.logInWithReadPermissionsAsync(FB_APP_ID, {
permissions: ['public_profile'],
});
if (type === 'success') {
const credential = firebase.auth.FacebookAuthProvider.credential(token);
try {
await firebase.auth().signInAndRetrieveDataWithCredential(credential);
} catch (error) {
console.log('cannot login ', error);
}
}
};
export const readData = (key) => {
console.log('getWins ');
const userId = firebase.auth().currentUser.uid;
return firebase
.database()
.ref(`/${key}/${userId}`)
.once('value');
};
...
class PostList extends React.Component {
async componentDidMount() {
// it normally resolves when logged with email & password,
// resolves with facebook auth on iPhone simulator
// does not resolve with facebook auth on Expo client on iPhone
const data = await readData('posts');
}
}
However, what is really strange, that it does not work on iPhone + Expo client, but does on the iPhone simulator. The crucial part is in the async componentDidMount().
Database config is still in the dev mode (allow all read & writes):
{
"rules": {
".read": true,
".write": true
}
}
I've used the following guides: https://docs.expo.io/versions/latest/sdk/facebook
https://docs.expo.io/versions/latest/guides/using-firebase#listening-for-authentication
Are there any more prerequisites that I've forgotten to setup? Or Expo client has limitations in terms of properly handling calls with Facebook auth?

Resources