AngularFire hasCustomClaim and customClaims not working - javascript

I have been trying to use Firebase custom claims and successfully able to create user-based roles like, manager, editor, admin. This is how my custom claim is saved in my Firebase Emulator Suite.
For user custom clams are set like this {"role": "manager"}, but whenever I tried to login and get the custom claim of the user it doesn't work.
Just an FYI I tried to set custom claims like this too {"manager": true} but hasCustomClaim("manager") always works if I use this in if and else condition even if I set {"manager": false} also if i try to login using admin privileges but still manager level works properly
const adminOnly = () => hasCustomClaim("admin");
const editorOnly = () => hasCustomClaim("editor");
const managerOnly = () => hasCustomClaim("manager" );
const editorOnly2d = pipe(customClaims, map(claims => claims.role == "editor"));
const mngOnly2 = pipe(customClaims, map(claims => claims.role == "manager"));
How shall I get current logged in user custom claims so that I can set routes based on user role?
This is what I get after login authUser.getIdTokenResult(true)
aud: "testing"
auth_time: 1629796111
email: "test12#gmail.com"
email_verified:false
exp: 1629799711
firebase: {identities: {…}, sign_in_provider: "password"}
iat: 1629796111
iss: "https://securetoken.google.com/testing"
role: "manager"
name: "Testing"
sub: "ZUlXd59HMhFI5gyozxW1xw0IXtPi"
user_id: "ZUlXd59HMhFI5gyozxW1xw0IXtPi"
I tried to search the issue, but all of them suggest the same thing that I used above.

The {"role": "manager"} will not work, because hasCustomClaim("admin") (as far as I can see) only checks for the existence of a claim named admin.
That also explains why it works when you use "manager": false, hasCustomClaim merely checks for the presence of the claim regardless of its value. Non-admins should not have an admin claim.
So if you want to use the existing AngularFire pipes, you'll need claims that identify whether the user is an admin, editor, and/or manager.
If you want to use your own role-based access control, that is possible too, but you'll have to implement your own auth pipe filters. For those, you'll want to start by logging the claims, so you can see what you're map call is working against.

Related

Limit pages to certain user type Nuxt & Firebase Firestore (Role-Based Authorization)

need some advice here.
My Nuxt & Firebase/Firestore web app will have 3 different type of users:
subcontractor
contractor
worker
First, I want my users, whenever they login, they will log into page related to their user type.
e.g: subcontractor login push to /subcontractor, contractor login push to /contractor etc etc.
I also want the user can only see pages related to their types. (user A only see /A & /Atwo, user B can only see /B & /Btwo, user C, can only see /C & /Ctwo etc etc..)
I want to avoid using cloud functions if can, as from what I understand, you cannot deploy your app in the free plan if your app has cloud functions in it.
Anyway, is below the right way to do it?
Create in firestore, Users document that contains details of user type,
e.g: "userType: subcontractor"
In the middleware, do the logic, based on user type.
(in my case, I need to have 3 diff middleware js file (isSubcontractor.js, isContractor.js, isWorker.js)
add middleware: "the-middleware file", inside my page
If its correct, how to do step 1 & 2?
Is there any articles or real-life application source code that explain briefly what I wanted?
Beginner here. Already gone thru here and there around the internet but can't quite find the answer that I wanted :(
Custom Claims are definitely an option but that would require Cloud functions or a server. Yes, you can store user type in their Firestore document and check it before the page renders or whenever required. However, you must make sure only authorized users can change their role.
The flow would be as simple as:
User logs in
Reading their role from Firestore document
Redirecting to relevant page
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import { getFirestore, doc, getDoc } from "firebase/firestore";
const auth = getAuth();
const firestore = getFirestore();
const login = async () => {
const { user } = await signInWithEmailAndPassword(auth, email, password);
// Reading user document
const docRef = doc(firestore, "users", user.uid);
const docSnap = await getDoc(docRef);
const { userType } = docSnap.data()
switch (userType) {
case 'contractor':
// redirect to /contractor
break;
case 'sub-contractor':
// redirect to /sub-contractor
break;
default:
// redirect to default page
break;
}
}
I also want the user can only see pages related to their types.
You can follow them same method in a server side middleware. First read userType and then check if user is authorized to visit the page. If not, redirect to any other page.
Best part of using Custom Claims is that you can read them in security rules of Realtime Database, Firestore and Storage as well. If you store user type in Firestore you cannot read that in security rules of any other Firebase service. Using Firestore also incurs additional charge for reading user's role every time. You need a Cloud function to set the custom claim only and not read the claim every time.

Firebase custom claim how to set?

I'm struggling with firebase custom claims.
I have tested a lot of approaches nothing works. Obviously, I miss something important in the concept itself.
So I'm back to the root. This script from the google example should apply customs rule on a newly created user
exports.processSignUp = functions.auth.user().onCreate(event => {
const user = event.data; // The Firebase user.
const customClaims = {
param: true,
accessLevel: 9
};
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, customClaims)
});
Then on a client, I check the result with
firebase.auth().currentUser.getIdTokenResult()
.then((idTokenResult) => {
// Confirm the user is an Admin.
console.log(idTokenResult.claims)
if (!!idTokenResult.claims.param) {
// Show admin UI.
console.log("param")
} else {
// Show regular user UI.
console.log("no param")
}
})
.catch((error) => {
console.log(error);
});
Everything just a raw copy-paste still doesn't work. I've tested both from the local machine(there could be troubles with cors?) and deployed
This is a race situation. If the Function end first then, you will get the updated data.
The getIdTokenResult method does force refresh but if the custom claim is not ready then, it is pointless.
You need to set another data control structure to trigger the force refresh on the client. By example a real-time listener to the rtd;
root.child(`permissions/${uid}`).on..
And the logic inside the listener would be: if the value for that node exists and is a number greater than some threshold, then trigger the user auth refresh
During that time the ui can reflect a loading state if there is no datasnapshot or the not admin view if the datasnapshot exists but is a lower permission level.
In Functions you have to set the node after the claim is set:
..setCustomUserClaims(..).then(
ref.setValue(9)
);
I have a more detailed example on pastebin
The claims on the client are populated when the client gets an ID token from the server. The ID token is valid for an hour, after which the SDK automatically refreshes it.
By the time the Cloud Functions auth.user().onCreate gets called, the client has already gotten the ID token for the new user. This means that it can take up to an hour before the client sees the updated claims.
If you want the client to get the custom claims before that, you can force it to refresh the token. But in this video our security experts recommend (that you consider) using a different storage mechanism for claims that you want to be applied straight away.

Check if Firebase Facebook user exists without creating a user starting from anonymous user

In Firebase I need to check if a Facebook user exists without creating the user. Initially the user is anonymous, and they try to login with Facebook. I want this to fail if the Facebook account is not already linked to a user in my system. It won't be linked to the current user because they are anonymous,
If I use Auth.signInAndRetrieveDataWithCredential I expected a "auth/user-not-found" error, but instead the user is simply created. Is this a bug or expected?
let credential = firebase.auth.FacebookAuthProvider.credential(
event.authResponse.accessToken)
firebase.auth().signInAndRetrieveDataWithCredential(credential).then( (userCredential) => {
let user = userCredential.user
app.debug("DEBUG: Existing user signed in:"+user.uid)
this.loginSuccess(user)
}).catch( (err) => {
app.error("ERROR re-signing in:"+err.code)
$("#login_status_msg").text(err)
})
If I use User.reauthenticateAndRetrieveDataWithCredential instead I get the error "auth/user-mismatch" which makes sense because user is currently anonymous. However, I was expecting "auth/user-not-found" may be thrown instead if the credential doesn't exist, but that doesn't happen.
I don't see a way to take my anonymous user, have them login with Facebook and then see if another user is already linked to that Facebook credential without creating the user if it doesn't exist.
If you're wondering why? My scenario is:
The system allows anonymous users
A user logs in, then converts to a logged in user by registering with Facebook.
App uninstall
App reinstall
User starts up the app and is initially anonymous.
They try and login with Facebook again. At this point I want to stop them from creating a user if they don't have one already. If they have a user ID already, the code works fine and changes their anonymous account ID to the original user ID which is good.
I found a solution! It wasn't too hard to implement, but it does seem hacky.
So we know that when using signInAndRetrieveDataWithCredential(cred) for facebook login, the account is created even if it does not exist yet. To solve this, we need to make sure that we handle the following three things:
Detect if the account is new
Delete the current account that was created by firebase
Throw an error to get out of the current flow and return to wherever you were before.
I just implemented and tested this solution, and it seems to work great:
// ... do your stuff to do fb login, get credential, etc:
const userInfo = await firebase.auth().signInAndRetrieveDataWithCredential(credential)
// userInfo includes a property to check if the account is new:
const isNewUser = _.get(userInfo, 'additionalUserInfo.isNewUser', true)
// FIRST, delete the account we just made.
// SECOND, throw an error (or otherwise escape the current context.
if (isNewUser) {
firebase.auth().currentUser.delete()
throw new Error('Couldn\'t find an existing account.')
}
// If the user already exists, just handle normal login
return userInfo.user
The reason I did this was to ensure that users had to go through the "create account flow" in my app. Your case would be really easy to implement as well, something like the following:
let credential = firebase.auth.FacebookAuthProvider.credential(event.authResponse.accessToken)
firebase.auth().signInAndRetrieveDataWithCredential(credential)
.then(userCredential => {
const isNewUser = userCredential.additionalUserInfo.isNewUser
if (isNewUser) {
firebase.auth().currentUser.delete()
// The following error will be handled in your catch statement
throw new Error("Couldn't find an existing account.")
}
// Otherwise, handle login normally:
const user = userCredential.user
app.debug("DEBUG: Existing user signed in:"+user.uid)
this.loginSuccess(user)
}).catch( (err) => {
app.error("ERROR re-signing in:"+err.code)
$("#login_status_msg").text(err)
})
You can use linkAndRetrieveDataWithCredential:
let credential = firebase.auth.FacebookAuthProvider.credential(
event.authResponse.accessToken);
anonymousUser.linkAndRetrieveDataWithCredential(credential).then( (userCredential) => {
// Firebase Auth only allows linking a credential if it is not
// already linked to another account.
// Now the anonymous account is upgraded to a permanent Facebook account.
}).catch( (err) => {
// Check for code: auth/credential-already-in-use
// When this error is returned, it means the credential is already
// used by another account.
})
You can use the method fetchSignInMethodsForEmail to check if an specific email is already associated to an specific provider or not. Doing this you will be able to check if one if the SighInMethods of the email associated to your user contains Facebook.com or not.
I show you below an example about how I manage this cases on my application. I'm using an RxJavaWrapper on my code, but you will understand the point of how to manage it:
RxFirebaseAuth.fetchSignInMethodsForEmail(authInstance, email)
.flatMap { providerResult ->
if (!providerResult.signInMethods!!.contains(credential.provider)) {
return#flatMap Maybe.error<AuthResult>(ProviderNotLinkedException(credential.provider))
} else {
return#flatMap RxFirebaseAuth.signInWithCredential(authInstance, credential)
}
}
.subscribe({ authResult ->
//Manage success
}, { error ->
//Manage error
})
First I check the providers associated to the email of the user(You can retrieve it from the provider)
If the list of SignInMethods contains my credential provider, I throw an error, if not, I call my signInWithCredential method to create the user.
Continue your workflow.
What I did to solve this problem without relying on the call to linkAndRetrieveDataWithCredential to fail and using the catch block to sign in the already existing user is to save the userID field that getCurrentAccessToken returns.
const { userID } = data;
this.props.setFacebookId(userID); // saves the userID on the server
I can later check if this userID already exists next time the user signs up with facebook.

Proper way to have initial properties for users in Firebase [with react]

I want to build an application where the users could login with Github (as of now, and probably Facebook, Google+ in the future).
I'm wondering how could I have initial properties when the user logs in first time, which would be modified in the future?
In my App component I have an onAuthStateChanged function which will push the users info into the database, but the issue is (obviously) that it will do this every time:
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
this.setState({
user
});
usersRef.push({
id: user.uid,
displayName: user.displayName,
coins: 3000
});
} else {
this.setState({
user: undefined
});
}
});
Is there a way I could check if the user logged in before? Is there a better way to achieve what I want?
Perhaps you can use a Firebase Authentication Trigger as documented at https://firebase.google.com/docs/functions/auth-events.
You could set up a trigger that runs once on the Firebase servers just after a user is created; that trigger can update data in the database for the user, which could include defaulting certain values. Your React app would then be able to read that data when it needs it.

firebase javascript injection

I want ask something about firebase security. How to handle following situations?
User is creating account with createUserWithEmailAndPassword() function, then i save his username,email,created_at...to realtime db. But what if data are not saved correctly. His account is created and he is logged in automatically but data is not stored.
I have some registration logic... for example unique usernames... so before creating acc i check if this username exist in realtime db. But he still can call createUserWithEmailandPassword() from js console and account is created.
For situation one:
According to the firebase docs (https://www.firebase.com/docs/web/api/firebase/createuser.html), creating a user does not automatically authenticate them. An additional call to authWithPassword() is required first. In order to ensure that a user isn't authenticated without valid data, you could run a check to the server to make sure the data is saved correctly before authenticating.
Edit: Nevermind that; looks like firebase does auto-auth now - take a look at what I wrote below.
Now a concern with this approach would be if your app allowed people to authenticate with an OAuth provider like gmail, then there is no function for creating the user before authenticating them. What you may need to do is pull the user data from the firebase, determine if it's valid, and if its not valid show a popup or redirect that lets the user fix any invalid data.
For situation two:
If you wanted to make sure that in the case of them calling createUserWithEmailAndPassword() from the console a new user is not created, you could try something like this with promises;
var createUserWithEmailAndPassword = function(username, password) {
var promise = isNewUserValid(username, password);
promise.then(function() {
// Code for creating new user goes here
});
}
In this way, you never expose the actual code that makes a new user because it exists within an anonymous function.
I don't think that this could solve the problem entirely though because firebases API would let anyone create an account using something
var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
ref.createUser({
email: "bobtony#firebase.com",
password: "correcthorsebatterystaple"
}
(Taken from https://www.firebase.com/docs/web/api/firebase/createuser.html)
If you wanted to make sure that server side you can't ever create a user with the same user name, you'd need to look into firebases's rules, specifically .validate
Using it, you could make sure that the username doesn't already exist in order to validate the operation of creating a username for an account.
Here's the firebase doc on rules: https://www.firebase.com/docs/security/quickstart.html
And this is another question on stack overflow that is quite similar to yours. Enforcing unique usernames with Firebase simplelogin Marein's answer is a good starting point for implementing the server side validation.
First save the user credentials in the realtime database before you create the user:
var rootRef = firebase.database().ref('child');
var newUser = {
[name]: username,
[email]: useremail,
[joined]: date
};
rootRef.update(newUser);
After adding the Usersinfo into the realtime database create a new user:
firebase.auth().createUserWithEmailAndPassword(useremail, userpassword).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
When an error occured while inserting the data in the realtime database, it will skip the createuser function.
This works fine for me, hope this helps!

Categories