Firebase custom claims doesn't seem to work - node.js

I have an issue with my project - Node.js and Firebase.
I want to add an admin role so I use custom claims according to the docs.
I have 3 functions in the cloud functions:
exports.addAdminRole = functions.https.onCall((data, context) => {
return admin.auth().setCustomUserClaims(context.auth.uid, {
admin: true
}).then(() =>'Success!')
.catch(err => err)
})
exports.getUserRecord = functions.https.onCall((data, context) => {
return admin.auth().getUser(context.auth.uid)
.then(userRecord => userRecord.customClaims)
.catch(err => err)
})
exports.deleteAdmin = functions.https.onCall((data, context) => {
return admin.auth().setCustomUserClaims(context.auth.uid, null)
.then(() => 'Deleted admin!')
.catch(err => err)
})
I call the functions directly in the client (http callable) and addAdminRole returns 'Success!' and seems to work. getUserRecord seems to work as well and returns {admin: true} as my custom claims.
Now for the problem. I defined a function to get the user claims in the client side like mentioned in the docs:
getRecords() {
firebaseInstance.auth.currentUser.getIdTokenResult()
.then(idTokenResult => {console.log("ADMIN:", idTokenResult)})
.catch(err => console.error(err))
}
(firebaseInstance is imported and works fine in the whole project)
I don't understand why but the code in the client side returns an object with the claims property but this property doesn't have the admin claim I added in the cloud functions.
if I try to access idTokenResult.claims.admin like in the docs it logs me UNDEFINED.
Link to the docs - https://firebase.google.com/docs/auth/admin/custom-claims#node.js
Could you clarify to me what's wrong?
Thank you for your help!

The claims of a user are stored in their ID token, which is auto-refreshed by the SDK every hour.
Setting a custom claim to a user's profile from the Admin SDK does not force an auto-refresh of the ID token of the clients, so it may take up to an hour before the claim shows up in the clients. The documentation hints at this with:
Once the latest claims have propagated to a user's ID token, you can get them by retrieving the ID token: ...
This documentation could be more explicit though, so I'd recommend leaving feedback with the button at the bottom of that page.
To ensure a claim propagates to a client sooner, you should force the client to refresh its ID token (for example, by passing true to getIdTokenResult().

Related

Getting a Azure AD refresh token and auth code using MSAL

I am working on an application with Ruby on Rails back-end and EmberJS Front-end.
I would like to achieve the following.
Log in user with MSAL with Ember FE
Get the auth_code and pass on to the back end
From the back end, fetch access token and refresh token.
Use it to send email using azure Graph API.
Basically I would like to perform step 1 of azure ad auth, i.e. fetching the auth_code, at the front end. And then perform step 2 at the back end. And then refresh the tokens at the back-end as needed.
I believe MSAL provides no good way to achieve this. When user consents (Step 1), it uses the auth_code by itself to fetch access_token and refresh_token (Step 2), and cashes it without exposing the values. It invokes acquireTokenSilent method to refresh the tokens when needed.
I can see that MSAL also has a ssoSilent method, which performs only step 1 of auth and returns auth code. I tried to use it in the following manner
signIn = () => {
myMSALObj
.loginPopup(loginRequest)
.then(this.handleResponse)
.catch((error) => {
console.error(error);
});
};
handleResponse = (resp) => {
console.log('>>>> Response', resp);
// this.token = resp;
if (resp !== null) {
username = resp.account.username;
// showWelcomeMessage(resp.account);
resp.account = myMSALObj.getAccountByUsername(username);
console.log('Resp => ', resp);
myMSALObj
.ssoSilent(resp)
.then((r) => {
console.log(r);
})
.catch((e) => {
console.log('Error ->', e);
});
} else {
// loadPage();
}
};
This always ends up in the following error
InteractionRequiredAuthError: interaction_required: Silent authentication was denied. The user must first sign in and if needed grant the client application access to the scope ...
Even when the user has just consented for these scopes.
Is there something I am missing?
or
Is there any better way to achieve this functionality?
Thanks in advance for any help
I am using Rails 6 with Ember 4.1

Google Sign-In idToken with createSessionCookie causing error - there is no user record corresponding to the provided identifier

Stack:
Google Sign-in (Vanilla JS - client side),
Firebase Functions (ExpressJS)
Client-Side:
My Firebase function express app uses vanilla javascript on the client side. To authenticate I am making use of Firebase's Google SignIn feature client-side javascript web apps, found here.
// Firebase setup
var firebaseConfig = {
apiKey: "AIza...",
authDomain: "....firebaseapp.com",
databaseURL: "https://...-default-rtdb.firebaseio.com",
...
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
function postIdTokenToSessionLogin(idToken, csrfToken) {
return axios({
url: "/user/sessionLogin", < ----- endpoint code portion found below
method: "POST",
data: {
idToken: idToken,
csrfToken: csrfToken,
},
});
}
// ...
// On sign-in click
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth()
.signInWithPopup(provider)
.then(async value => {
const idToken = value.credential.idToken;
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
}).then(value => {
window.location.assign("/user/dashboard")
}).catch((error) => {
alert(error.message);
});
Note I am using value.credential.idToken (most sources imply to use this, but haven't found an example saying use this specifically)
Directly after calling signInWithPopup, a new account is created in my Firebase Console Authentication matching the gmail account that was just signed in.
Server-side:
Once I authenticate, I create an axios request passing in the {user}.credential.idToken and following the server-side setup here (ignoring the CSRF - this just doesn't want to work).
In creating the session, I use the following code in my firebase functions express app, the endpoint which is router.post('/sessionLogin', (req, res) => (part of /user route prefix):
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const idToken = req.body.idToken.toString(); // eyJhbGciOiJSUzI1NiIsImt...[936]
admin
.auth()
.createSessionCookie(idToken, {expiresIn}) < ----------- Problem line
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
On the createSessionCookie call, I get the following error & stack trace:
Error: There is no user record corresponding to the provided identifier.
at FirebaseAuthError.FirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:44:28)
at FirebaseAuthError.PrefixedFirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:90:28)
at new FirebaseAuthError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:149:16)
at Function.FirebaseAuthError.fromServerError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:188:16)
at C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\auth\auth-api-request.js:1570:49
at processTicksAndRejections (internal/process/task_queues.js:93:5)
This is part of the sign-in flow with a existing Gmail account.
What is causing this?
After many hours of searching, Googling - I have seen the light.
For some additional context, this error featured heavily in my struggle "Firebase ID token has invalid signature." - I will get to that in a second.
Further, another issue I also faced was using a local auth emulator for web client-side (javascript), see this for setup.
TL;DR to solve the immediate problem
Client-side remained largely the same, however the documentation provided by Firebase was inaccurate/misleading - thanks to this post, I found the solution. Thus, it follows...
Which is the ID Token? (Client-side):
The examples from here (to allow signInWithPopup), the response (if successful) results in
...
.signInWithPopup(provider)
.then((result) => {
/** #type {firebase.auth.OAuthCredential} */
var credential = result.credential;
// This gives you a Google Access Token. You can use it to access the Google API.
var token = credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
Looking for an idToken, I found one using result.credential.idToken but no where on the internet on if this was infact the correct token to use.
I ran into this error using the provided idToken above:
Firebase ID token has incorrect "aud" (audience) claim. Expected
"[insert your **projectId**]" but got
"59895519979-2l78aklb7cdqlth0eob751mdm67kt301.apps.googleusercontent.com".
Make sure the ID token comes from the same Firebase project as the
service account used to authenticate this SDK.
Trying other tokens like result.credential.accessToken responded with various verification errors - what to do?
Mention earlier, this solution on Github suggested to use firebase.auth().currentUser.getIdToken() AFTER you have signed in. An example (building on my previous code) is to do the following:
...
.signInWithPopup(provider)
.then((result) => {
// current user is now valid and not null
firebase.auth().currentUser.getIdToken().then(idToken => {
// send this ID token to your server
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
})
})
At this point, you can verify your token and createSessionCookies to your heart's desire.
BUT, a secondary issue I unknowingly created for myself using the Authentication Emulator.
To setup for client-side use:
var auth = firebase.auth();
auth.useEmulator("http://localhost:9099");
To setup for hosting your firebase functions app (assuming you are using this with e.g. nodejs + express, see this for setup, ask in comments, can provide more details if needed)
Using Authentication Emulator caused the following errors AFTER using the above mentioned "fix". Thus, DO NOT RUN the local authentication emulator (with Google sign-in of a valid Google account) as you will consistently get.
Firebase ID token has invalid signature. See
https://firebase.google.com/docs/auth/admin/verify-id-tokens for
details on how to retrieve an ID token
You can use all your local emulators, but (so far in my experience) you will need to use an online authenticator.

How to listen for new users when using Firebase's listUsers()

The Firebase admin SDK for Node.js provides us with a way of retrieving every user in our project, as seen in the documentation here.
I have implemented this in my own code as follows:
const listAllUsers = (nextPageToken) => {
return new Promise((resolve, reject) => {
// List batch of users, 1000 at a time.
const customerUIDs = []
admin.auth().listUsers(1000, nextPageToken)
.then((listUsersResult) => {
listUsersResult.users.forEach((userRecord) => {
// check for customers by their claims
if (userRecord.toJSON().customClaims.customer) {
customerUIDs.push(userRecord.toJSON().uid)
}
})
if (listUsersResult.pageToken) {
// List next batch of users.
listAllUsers(listUsersResult.pageToken)
}
resolve(customerUIDs)
})
.catch((error) => {
reject(error)
})
})
}
...
// some
// more
// code
NB: When a user is created, we assign the custom claims of customer: true to specify that the user is a customer, not an admin. That happens in my Cloud functions, so no need to paste it here again.
My question is this:
This function above is a one-time operation. How do I listen for new users, and add them to the customerUIDs array?
The Admin SDK doesn't provide any way to listen for newly created users.
If you want to write some code that runs when a new user account is created, you should look into using an Authentication trigger provided by Cloud Functions.

Firebase Re-authentication & email Linking for Phone-Authenticated Users

When users sign up, they use Phone Auth. After using the app for a while, they are advised to link an (email & password) to their existing account.
The linking process fails because of the error (auth/requires-recent-login.) My code follows.
// The following generates the error: [auth/requires-recent-login] This operation is sensitive and requires recent authentication. Log in again before retrying this request.
const emailCredential = firebase.auth.EmailAuthProvider.credential(state.email, state.password);
const newCredential = await firebase.auth().currentUser.linkWithCredential(emailCredential);
To fix this error, I understand that I need to call reauthenticateWithCredential() before linking. However, I don't want to ask the user to log in again (receive & enter a verification code.) Is this at all possible?
I tried passing the result of currentUser.getIdToken(true) to PhoneAuthProvider.credential() I am not sure if this is right. Anyway, it generated an error (Cannot create PhoneAuthCredntial without either verificationProof, sessionInfo, temporary proof, or enrollment ID.).
My code follows.
// The following works:
const accessToken = await firebase.auth().currentUser.getIdToken(true);
// The following works:
const currentCredential = firebase.auth.PhoneAuthProvider.credential(accessToken);
// The following generates the error: Cannot create PhoneAuthCredential without either verificationProof, sessionInfo, temporary proof, or enrollment ID.
const abc = await firebase.auth().currentUser.reauthenticateWithCredential(currentCredential);
// The following is never reached:
const emailCredential = firebase.auth.EmailAuthProvider.credential(state.email, state.password);
const newCredential = await firebase.auth().currentUser.linkWithCredential(emailCredential);
Thank you for your effort and time to help me...
Important Information:
firebase.auth().currentUser.reauthenticateWithCredential(credential) requires the attribute credential
For users, who logged in using a Phone Number, I could not find a way to get this credential when required. By the way, it is possible to get it for users, who logged in using other providers, e.g. Facebook.
However, for users, who log in using a phone number, it is possible to get this credential during the login process. Check https://firebase.google.com/docs/auth/web/phone-auth.
So, I decided to save the credential for the user on their device during the login process. I am using Redux and Redux-Persist for that.
My code after fixing it.
// This is an extract from the login script:
firebase.auth().signInWithPhoneNumber(phoneNo)
.then(confirmResult => {
dispatch({ type: "PhoneNo_accepted", payload: { confirmResult: confirmResult } });
})
.catch(error => {
dispatch({ type: "display_message", payload: { messageText: `Phone Number Error: ${error.message}` } });
});
// Change#1. The following statement is a NEW step, which I added to get the credential during the login process.
const credential = firebase.auth.PhoneAuthProvider.credential(state.confirmResult.verificationId, state.codeInput);
state.confirmResult.confirm(state.codeInput)
.then( (user) => {
// Change#2. The following function would save the credential to the app's state, e.g. using Redux
_onAuthComplete(user, credential);
})
.catch( error => {
dispatch({ type: "display_message", payload: { messageText: `Verification Code Error: ${error.message}` } });
});
// // //
// This is an extract from the linking script:
// Change#3. props.credential is the credential, which was saved to the app's state.
await firebase.auth().currentUser.reauthenticateWithCredential(props.credential);
const emailCredential = firebase.auth.EmailAuthProvider.credential(state.email, state.password);
const newCredential = await firebase.auth().currentUser.linkWithCredential(emailCredential);

Access Facebook Messenger User Profile API in DialogFlow

I'm building a cross-platform chatbot in Google's DialogFlow. I'd like to access the Facebook User Profile API to learn the user's first name.
I'm struggling to find advice on how (or if) I can make this happen.
https://developers.facebook.com/docs/messenger-platform/identity/user-profile/
Has anybody here achieved this?
I did that for one of my bots yesterday, you need 2 things, first the Page Token and second is the psid which is Page scope user ID.
On dialogflow, you will receive the request block with psid as sender id. You can find it at:
agent.originalRequest.payload.data.sender.id
This psid needs to be passed to api get request at
/$psid?fields=first_name with your page Token as accessToken to get the first name in response.
You need to make a call to Facebook Graph API in order to get user's profile.
Facebook offers some SDKs for this, but their official JavaScript SDK is more intended to be on a web client, not on a server. They mention some 3rd party Node.js libraries on that link. I'm particularly using fbgraph (at the time of writing, it's the only one that seems to be "kind of" maintained).
So, you need a Page Token to make the calls. While developing, you can get one from here:
https://developers.facebook.com/apps/<your app id>/messenger/settings/
Here's some example code:
const { promisify } = require('util');
let graph = require('fbgraph'); // facebook graph library
const fbGraph = {
get: promisify(graph.get)
}
graph.setAccessToken(FACEBOOK_PAGE_TOKEN); // <--- your facebook page token
graph.setVersion("3.2");
// gets profile from facebook
// user must have initiated contact for sender id to be available
// returns: facebook profile object, if any
async function getFacebookProfile(agent) {
let ctx = agent.context.get('generic');
let fbSenderID = ctx ? ctx.parameters.facebook_sender_id : undefined;
let payload;
console.log('FACEBOOK SENDER ID: ' + fbSenderID);
if ( fbSenderID ) {
try { payload = await fbGraph.get(fbSenderID) }
catch (err) { console.warn( err ) }
}
return payload;
}
Notice you don't always have access to the sender id, and in case you do, you don't always have access to the profile. For some fields like email, you need to request special permissions. Regular fields like name and profile picture are usually available if the user is the one who initiates the conversation. More info here.
Hope it helps.
Edit
Promise instead of async:
function getFacebookProfile(agent) {
return new Promise( (resolve, reject) => {
let ctx = agent.context.get('generic');
let fbSenderID = ctx ? ctx.parameters.facebook_sender_id : undefined;
console.log('FACEBOOK SENDER ID: ' + fbSenderID);
fbGraph.get( fbSenderID )
.then( payload => {
console.log('all fine: ' + payload);
resolve( payload );
})
.catch( err => {
console.warn( err );
reject( err );
});
});
}

Resources