Firebase functions setting custom claims - javascript

I have a problem with the function(I suspect) in which I add custom claims. Here is the code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.addAdminRole = functions.https.onCall((data)=> {
//get user and add custom claim (admin)
return admin.auth().getUserByEmail(data.email).then(user => {
return admin.auth().setCustomUserClaims(user.uid, {
admin: true
});
}).then( () => {
return {
message: `Success! ${data.email} has been made an admin`,
}
})
})
I call the function using the following code(I use Redux and React):
let addAdminRole = window.firebaseFunctions.httpsCallable('addAdminRole');
addAdminRole({email:employee.email}).then(res => {
console.log(res)
})
I get the expected message({message: Success! ${data.email} has been made an admin}), but the claim isn't added.
I make a separate Firebase Auth REST API via axios for authentication, but the claim 'admin' isn't there.
I have a Spark(free) billing plan and when checking the logs from firebase functions I see 'Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions' when addAdminRole is executed.
From what I read this is a message that you always get on the free plan and there shouldn't be a problem when accessing internal(google) info.
Here is the code for the axios Request:
axios({
method:'post',
url:urlAuth,
data:{
email:employee.email,
password:employee.password,
returnSecureToken: true
}
}).then(res => {
delete employee.password;
console.log(res)
const expirationDate = new Date().getTime() + res.data.expiresIn * 1000;
localStorage.setItem('token',res.data.idToken);
localStorage.setItem('expirationDate',expirationDate);
localStorage.setItem('userId', res.data.localId);
localStorage.setItem('admin', res.data.admin)
dispatch(checkAuthTimeout(res.data.expiresIn));
if(logIn){
dispatch(endAuth(res.data.idToken,res.data.localId,res.data.admin));
}else{
employee.userId = res.data.localId;
dispatch(addEmplRegister(employee,res.data.idToken,admin));
}
}).catch(err => {
dispatch(errorAuth(err.message))
})
FOUND OUT THE ISSUE, the information about claims isn't transmitted when using REST API authentication

Setting a custom claim doesn't take effect immediately for users with existing JWT tokens. Those users will have to either:
Sign out and back in again,
Force refresh their token, or
Wait up to one hour for that token to automatically refresh by the Fireabse Auth SDK.
On then will their new token show the custom claim.

Related

How can I locally test cloud function invocation from third-party in Firebase?

I am developing an API for a third-party application not related to Firebase. This API consist of cloud functions to create and add users to database, retrieve user information and so on. These functions are created using the admin SDK. Example of a function that adds a user looks like this:
export const getUser = functions.https.onRequest(async (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.set('Access-Control-Max-Age', '3600');
res.status(204).send('');
} else {
const utils = ethers.utils;
const method = req.method;
const body = req.body;
const address = body.address;
const userAddress = utils.getAddress(address);
let logging = "received address: " + address + " checksum address: " + userAddress;
let success = false;
const db = admin.firestore();
const collectionRef = db.collection('users');
// Count all matching documents
const query = collectionRef.where("userAddress", "==", userAddress);
const snapshot = await query.get();
// If no documents match, there is no matching user
console.log(snapshot.docs.length);
if (snapshot.docs.length != 1) {
logging += "User does not exist in database.";
res.send({success: success, logging: logging});
return;
}
const data = snapshot.docs[0].data();
if (data != undefined) {
const createdAt = data.createdAt;
const emailAddress = data.emailAddress;
const userAddress = data.userAddress;
const updatedAt = data.updatedAt;
const userName = data.userName;
success = true;
res.send({success: success, createdAt: createdAt, emailAddress: emailAddress, userAddress: userAddress, updatedAt: updatedAt, userName: userName, logging: logging});
}
}
});
NOTE: These functions are NOT going to be called by the third-party application users, only by the third-party application itself.
I am pretty new at programming so I understand that this may not be the best way to code this functionality and I'm greatful for any tips you might have here as well. Anyway, back to my question. I'm trying to mimic the way that my customer is going to invoke these functions. So to test it, I'm using the following code:
function runGetUser() {
// test values
const address = 'myMetaMaskWalletAddress';
axios({
method: 'POST',
url: 'http://127.0.0.1:5001/cloud-functions/us-central1/user-getUser',
data: { "address": address },
}).then((response) => {
console.log(response.data);
}).catch((error) => {
console.log(error);
});
};
This works fine. However, I do not want anyone to be able to invoke these functions when I actually deploy them later. So I have been reading Firebase docs and googling on how to setup proper authentication and authorization measures. What I have found is setting up a service account and using gcloud CLI to download credentials and then invoke the functions with these credentials set. Is there not a way that I could configure this so that I query my API for an authorization token (from the file where the axios request is) that I then put in the axios request and then invoke the function with this? How do I do this in that case? Right now also, since I'm testing locally, on the "cloud function server-side" as you can see in my cloud function example, I'm allowing all requests. How do I filter here so that only the axios request with the proper authorization token/(header?) is authorized to invoke this function?
Thank you for taking the time to read this. Best regards,
Aliz
I tried following the instructions on this page: https://cloud.google.com/functions/docs/securing/authenticating#gcloud where I tried to just invoke the functions from the Gcloud CLI. I followed the instructions and ran the command "gcloud auth login --update-adc", and got the response: "Application default credentials (ADC) were updated." Then I tried to invoke a function I have "helloWorld" to just see that it works with the following command: curl -H "Authorization: bearer $(gcloud auth print-identity-token)" \http://127.0.0.1:5001/cloud-functions/us-central1/helloWorld", and I got the following response: "curl: (3) URL using bad/illegal format or missing URL". So I don't know what to do more.

Authenticated requests after sign in with React Query and NextAuth

I'm having troubled sending an authenticated request to my API immediately after signing in to my Nextjs app using NextAuth. The request that is sent after signing in returns data for and unauthenticated user.
I believe the issue is that React Query is using a previous version of the query function with an undefined jwt (which means its unauthenticated). It makes sense because the query key is not changing so React Query does not think it's a new query, but, I was under the impression that signing in would cause loading to be set to true temporarily then back to false, which would cause React Query to send a fresh request.
I've tried invalidating all the queries in the app using queryClient, but that did not work. I've also used React Query Devtools to invalidate this specific query after signing in but it still returns the unauthenticated request. Only after refreshing the page does it actually send the authenticated request.
// useGetHome.js
const useGetHome = () => {
const [session, loading] = useSession();
console.log(`session?.jwt: ${session?.jwt}`);
return useQuery(
'home',
() => fetcher(`/home`, session?.jwt),
{
enabled: !loading,
},
);
}
// fetcher
const fetcher = (url, token) => {
console.log(`token: ${token}`);
let opts = {};
if (token) {
opts = {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}${url}`, opts);
if (!res.ok) {
const error = await res.json();
throw new Error(error.message);
}
return res.json();
}
// Home.js
const Home = () => {
const { data: home_data, isLoading, error } = useGetHome();
...
return(
...
)
}
Attached is the console immediately after signing in. You can see the the session object contains the jwt after signing in, but in the fetcher function it is undefined.
console after signing in
Any help here is appreciated. Is there a better way to handle authenticated requests using React Query and NextAuth? Thank you!
I have tried a similar situation here and struggled the same thing but the enabled property worked fine for me and it is good to go right now.
https://github.com/maxtsh/music
Just check my repo to see how it works, that might help.

Getting "Error: Invalid IdP response/credential" trying to add FB user on Firebase Auth list

I've been working on implementing Facebook & Google sign-in method in an app based on Expo.
Having Firebase connected with my app, I want the user to be listed in Firebase Users. While Google sign-in seems to work fine, Facebook gives an error as stated below from firebase.auth().signInWithCredential(credential):
[Error: Invalid IdP response/credential: http://localhost?id_token=EAAGUBzIZAgb0BABAkdrj5NcUcvljcsLeNBQlV5VUZAZC7M8e7sSRg2MkqlFCuD7tKjin4uZBep5gSs20oAo8fXKiUqq2deEbUl6HoaAUskTda7x49VCqqcbYh1W3566fMZBtRFB5S3fRV7D41AGVGPMAck91l1KiFCzQzCGtSf5g6ZBKoyHw03LOVONcOiwVZB4vXVcGPYmIzL3RuzzztdBNLRql5ndSk0ZD&providerId=facebook.com]
Here's the relevant part of the code:
Firebase.js
// imports, configs and exports omitted
export const facebookProvider = new firebase.auth.FacebookAuthProvider;
LoadingScreen.js (the sign-in part)
import firebase, { googleProvider, facebookProvider } from "../firebase";
import * as Facebook from "expo-facebook";
const LoadingScreen = ({ navigation }) => {
const signInWithFacebook = async () => {
try {
await Facebook.initializeAsync("444233602924989");
const { type, token } = await Facebook.logInWithReadPermissionsAsync(
"444233602924989",
{
permissions: ["public_profile", "email"]
}
);
if (type === "success") {
const credential = facebookProvider.credential(token);
onSignInFB(credential);
} else {
Alert.alert('FB sign-in failed')
}
} catch ({ message }) {
console.log(`Facebook 1 Error: ${message}`);
Alert.alert(`first ${message}`);
}
};
const onSignInFB = credential => {
firebase
.auth()
.signInWithCredential(credential)
.then(function() {
navigation.navigate("Home");
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
});
};
};
I've logged some of the elements as follows:
While none of the error properties were available, the error code was auth/internal-error. Console.logging the error itself produced the error Invalid IdP response/credential.
The token from logInWithReadPermissionsAsync was a valid access token (string)
The credential from facebookProvider.credential(token) was:
Object {"oauthIdToken": "EAAGUBzIZAgb0BAKgeMa5E7qHm8WNJYv5SeuQ8DHyhkUlAnkMhE7niu6tx3e2amSMHSEqG9B0MV4a9dygwgjs337PR7AA3M4PZB2F6x6n1FwAEyZBKhZBpOSE2OWQ9dJipirpafg61TKX36hnKIzaIcwRkjs8YYRBbDuLnZAhJzWst3ZBM5tafwxYKumv2F4kYdexxZAXqb1nosnwYodNvB9bstkcaBrfB8ZD",
"providerId": "facebook.com",
"signInMethod": "facebook.com",
}
firebase.auth().currentUser was null.
I've also gone through my own sanity checklist of possible mistakes below, but couldn't find any:
FB is enabled in Firebase Auth Sign-in
AppId & App Secret is correctly copied from FB app to Firebase(and also the redirect URI)
Pass in the access token from Facebook.logInWithReadPermissionsAsync to Facebook's access token debugger and see if anything is wrong (e.g. expired or invalid)
Check if an actual credential is returned from facebookProvider.credential(token) and passed to signInWithCredential
It would be great to find out the source of the problem! (one possibility might be the access token returned from logInWithReadPermissionsAsync is identical with the oauthIdToken property in the credential object, but I haven't manually put or changed any one of those.)
Thanks for reading :)
I banged my head against this today and the solution was to pass an object in to the credential call, even though the docs say to pass in a string. In addition, the object property needs to be keyed as accessToken.
So instead of:
const credential = facebookProvider.credential(token);
try:
const credential = facebookProvider.credential({ accessToken: token });
Based on the docs, I have no idea why that key is needed or why it has to be an object, but it worked in my case. Hope it helps!

result.user.link is not a function in angular firebase

Not able to link facebook account with an existing firebase account. I currently have a firebase account in that I created using google credentials. Now I want to link a facebook account with this exiting firebase account (both having same creadentials) and I followed the steps mentioned here :
https://firebase.google.com/docs/auth/web/facebook-login
But at last, when I call method "result.user.link(pendingCred).then( (user) => { .. }" to link account I get following error in console :
Uncaught TypeError: result.user.link is not a function
at auth.service.ts:188
at e.g (auth.js:23)
at Yb (auth.js:26)
at Ub (auth.js:26)
at z.webpackJsonp.../../../../#firebase/auth/dist/auth.js.h.Mb (auth.js:25)
at Cb (auth.js:19)
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:392)
at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:142)
at zone.js:873
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:425)
Here is my code
loginFacebook(): Promise<any> {
return this.afAuth.auth.signInWithPopup(new firebase.auth.FacebookAuthProvider())
.then( (result) => {
this.registerUserName = result.user.displayName;
this.authDbUsername = result.user.email.replace('.', '');
// this.setLoggedinUser(user)
})
.catch ( (error) => {
if (error.code === 'auth/account-exists-with-different-credential') {
alert('You have already signed up with a different auth provider for that email.');
const pendingCred = error.credential;
// The provider account's email address.
const email = error.email;
// Get registered providers for this email.
firebase.auth().fetchProvidersForEmail(email).then( (providers) => {
// Step 3.
// If the user has several providers,
// the first provider in the list will be the "recommended" provider to use.
if (providers[0] === 'password') {
// Asks the user his password.
// In real scenario, you should handle this asynchronously.
const password = this.promptUserForPassword(providers[0]); // TODO: implement promptUserForPassword.
firebase.auth().signInWithEmailAndPassword(email, password).then( (user) => {
// Step 4a.
return user.link(pendingCred);
}).then(function( user) {
// Google account successfully linked to the existing Firebase user.
this.authState = user;
});
return;
}
// All the other cases are external providers.
// Construct provider object for that provider.
// TODO: implement getProviderForProviderId.
const provider = new firebase.auth.GoogleAuthProvider(); // this.getProviderForProviderId(providers[0]);
// At this point, you should let the user know that he already has an account
// but with a different provider, and let him validate the fact he wants to
// sign in with this provider.
// Sign in to provider. Note: browsers usually block popup triggered asynchronously,
// so in real scenario you should ask the user to click on a "continue" button
// that will trigger the signInWithPopup.
firebase.auth().signInWithPopup(provider).then( (result) => {
// Remember that the user may have signed in with an account that has a different email
// address than the first one. This can happen as Firebase doesn't control the provider's
// sign in flow and the user is free to login using whichever account he owns.
// Step 4b.
// Link to Google credential.
// As we have access to the pending credential, we can directly call the link method.
const resultingUser = result.user;
result.user.link(pendingCred).then( (user) => {
// Google account successfully linked to the existing Firebase user.
this.authState = user; // goToApp();
}).catch( (errorInLinking) => {
console.log(errorInLinking);
});
});
});
}
});
}
Please let me know if I am missing anything.
Thanks!
Change to result.user.linkWithCredential(pendingCred).then( (user) ...
link has been deprecated in favor of linkWithCredential starting from version 4.0.0.

How to just create an user in Firebase 3 and do not authenticate it?

I am working on a angularfire project and I would like to know how can I create an user in Firebase 3 and once done, do not authenticate the specified user. In the previous Firebase version we had the method called createUser(email, password). Now we have the method createUserWithEmailAndPassword(email, password) only, it creates and authenticates the specified user.
The answer to the question is: you can't.
We have similar situation where we have 'admin' users that can create other users. With 2.x this was a snap. With 3.x it's a fail as that capability was completely removed.
If you create a user in 3.x you authenticate as that user, and unauthenticate the account that's logged in.
This goes deeper as you would then need to re-authenticate to create another user; so the admin either does that manually or (cringe) stores the authentication data locally so it could be an automated process (cringe cringe, please don't do this)
Firebase has publicly stressed that 2.x will continue to be supported so you may just want to avoid 3.x.
Update:
one of the Firebaser's actually came up with a workaround on this. Conceptually you had an admin user logged in. You then create a second connection to firebase and authenticate with another user, that connection then creates the new user. Rinse - repeat.
Update again
See this question and answer
Firebase kicks out current user
You can do this using cloud function and firebase admin SDK.
Create HTTP function like below.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions
exports.createUser = functions.https.onRequest((request, response) => {
if (request.method !== "POST") {
response.status(405).send("Method Not Allowed");
} else {
let body = request.body;
const email = body.email;
const password = body.password;
const displayName = body.displayName;
admin.auth().createUser({
email: email,
emailVerified: false,
password: password,
displayName: displayName,
disabled: false
})
.then((userRecord) => {
return response.status(200).send("Successfully created new user: " +userRecord.uid);
})
.catch((error) => {
return response.status(400).send("Failed to create user: " + error);
});
}
});
In your client app, call this function using Http request, for example using ajax
$.ajax({
url: "the url generated by cloud function",
type: "POST",
data: {
email: email,
password: password,
displayName: name
},
success: function(response) {
console.log(response);
},
error: function(xhr, status, error) {
let err = JSON.parse(xhr.responseText);
console.log(err.Message);
}
});

Categories

Resources